relayer: remove the spy_relayer
The payload version 1 relayer has long been superceded by the more modular relayer-engine and specialized / automatic relayers. The newer relayers all rely on wormhole payload version 3 and no one is currently running a payload version 1 relayer. Additionally, support for it has been removed from portal bridge by the portal maintainers. The code lives on in the better maintained and supported relayer-engine.
This commit is contained in:
parent
81f411dbf9
commit
607a339cfb
|
@ -23,7 +23,6 @@
|
|||
|
||||
/clients/ @kcsongor @kev1n-peters @evan-gray
|
||||
/lp_ui/ @evan-gray @kev1n-peters
|
||||
/relayer/spy_relayer @evan-gray @kev1n-peters
|
||||
/relayer/generic_relayer @chase-45 @derpy-duck @JoeHowarth
|
||||
/scripts/ @evan-gray @kcsongor
|
||||
/sdk/ @bruce-riley @evan-gray
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
node_modules
|
|
@ -1,14 +0,0 @@
|
|||
SUPPORTED_CHAINS=[{"chainId":1,"chainName":"Solana","nodeUrl":"http://localhost:8899","nativeCurrencySymbol":"SOL","tokenBridgeAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE","bridgeAddress":"Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","wrappedAsset":"So11111111111111111111111111111111111111112"},{"chainId":2,"chainName":"ETH","nativeCurrencySymbol":"ETH","nodeUrl":"http://localhost:8545","tokenBridgeAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16","wrappedAsset":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":3,"chainName":"Terra","isTerraClassic":true,"nativeCurrencySymbol":"LUNA","nodeUrl":"http://localhost:1317","tokenBridgeAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4","terraName":"localterra","terraChainId":"localterra","terraCoin":"uluna","terraGasPriceUrl":"http://localhost:3060/v1/txs/gas_prices"},{"chainId":4,"chainName":"BSC","nativeCurrencySymbol":"BNB","nodeUrl":"http://localhost:8545","tokenBridgeAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16","wrappedAsset":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"}]
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
PROM_PORT=8083
|
||||
REST_PORT=4201
|
||||
READINESS_PORT=2000
|
||||
CLEAR_REDIS_ON_INIT=true
|
||||
DEMOTE_WORKING_ON_INIT=true
|
||||
LOG_LEVEL=debug
|
||||
SUPPORTED_TOKENS=[{"chainId":1,"address":"So11111111111111111111111111111111111111112"},{"chainId":1,"address":"2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"},{"chainId":2,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":2,"address":"0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A"},{"chainId":3,"address":"uluna"},{"chainId":4,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"}]
|
||||
PRIVATE_KEYS=[{"chainId":1,"privateKeys":[[14,173,153,4,176,224,201,111,32,237,183,185,159,247,22,161,89,84,215,209,212,137,10,92,157,49,29,192,101,164,152,70,87,65,8,174,214,157,175,126,98,90,54,24,100,177,247,77,19,112,47,44,165,109,233,102,14,86,109,29,134,145,132,141]]},{"chainId":2,"privateKeys":["0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"]},{"chainId":3,"privateKeys":["notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"]},{"chainId":4,"privateKeys":["0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"]}]
|
||||
SPY_SERVICE_HOST=guardian-spy:7073
|
||||
SPY_SERVICE_FILTERS=[{"chainId":1,"emitterAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"},{"chainId":2,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"},{"chainId":3,"emitterAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"},{"chainId":4,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"}]
|
||||
SPY_NUM_WORKERS=5
|
|
@ -1,14 +0,0 @@
|
|||
SUPPORTED_CHAINS=[{"chainId":1,"chainName":"Solana","nodeUrl":"http://localhost:8899","nativeCurrencySymbol":"SOL","tokenBridgeAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE","bridgeAddress":"Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","wrappedAsset":"So11111111111111111111111111111111111111112"},{"chainId":2,"chainName":"ETH","nativeCurrencySymbol":"ETH","nodeUrl":"http://localhost:8545","tokenBridgeAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16","wrappedAsset":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":3,"chainName":"Terra","isTerraClassic":true,"nativeCurrencySymbol":"LUNA","nodeUrl":"http://localhost:1317","tokenBridgeAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4","terraName":"localterra","terraChainId":"localterra","terraCoin":"uluna","terraGasPriceUrl":"http://localhost:3060/v1/txs/gas_prices"},{"chainId":4,"chainName":"BSC","nativeCurrencySymbol":"BNB","nodeUrl":"http://localhost:8545","tokenBridgeAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16","wrappedAsset":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"}]
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
PROM_PORT=8083
|
||||
REST_PORT=4201
|
||||
READINESS_PORT=2000
|
||||
CLEAR_REDIS_ON_INIT=true
|
||||
DEMOTE_WORKING_ON_INIT=true
|
||||
LOG_LEVEL=debug
|
||||
SUPPORTED_TOKENS=[{"chainId":1,"address":"So11111111111111111111111111111111111111112"},{"chainId":1,"address":"2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"},{"chainId":2,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":2,"address":"0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A"},{"chainId":3,"address":"uluna"},{"chainId":4,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"}]
|
||||
PRIVATE_KEYS=[{"chainId":1,"privateKeys":[[14,173,153,4,176,224,201,111,32,237,183,185,159,247,22,161,89,84,215,209,212,137,10,92,157,49,29,192,101,164,152,70,87,65,8,174,214,157,175,126,98,90,54,24,100,177,247,77,19,112,47,44,165,109,233,102,14,86,109,29,134,145,132,141]]},{"chainId":2,"privateKeys":["0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"]},{"chainId":3,"privateKeys":["notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"]},{"chainId":4,"privateKeys":["0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"]}]
|
||||
SPY_SERVICE_HOST=localhost:7072
|
||||
SPY_SERVICE_FILTERS=[{"chainId":1,"emitterAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"},{"chainId":2,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"},{"chainId":3,"emitterAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"},{"chainId":4,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"}]
|
||||
SPY_NUM_WORKERS=5
|
|
@ -1,13 +0,0 @@
|
|||
SPY_SERVICE_HOST=spy:7072
|
||||
SPY_SERVICE_FILTERS=[{"chainId":1,"emitterAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"},{"chainId":2,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"},{"chainId":3,"emitterAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"},{"chainId":4,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"},{"chainId":18,"emitterAddress":"terra1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrquka9l6"}]
|
||||
SPY_NUM_WORKERS=5
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
REST_PORT=4201
|
||||
PROM_PORT=8082
|
||||
READINESS_PORT=2000
|
||||
|
||||
LOG_LEVEL=debug
|
||||
SUPPORTED_TOKENS=[{"chainId":1,"address":"So11111111111111111111111111111111111111112"},{"chainId":1,"address":"2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"},{"chainId":2,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":2,"address":"0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A"},{"chainId":3,"address":"uluna"},{"chainId":4,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":18,"address":"uluna"}]
|
|
@ -1,10 +0,0 @@
|
|||
SUPPORTED_CHAINS=[{"chainId":1,"chainName":"Solana","nodeUrl":"http://solana-devnet:8899","nativeCurrencySymbol":"SOL","tokenBridgeAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE","bridgeAddress":"Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","wrappedAsset":"So11111111111111111111111111111111111111112"},{"chainId":2,"chainName":"ETH","nativeCurrencySymbol":"ETH","nodeUrl":"http://eth-devnet:8545","tokenBridgeAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16","wrappedAsset":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":3,"chainName":"Terra Classic","isTerraClassic":true,"nativeCurrencySymbol":"LUNC","nodeUrl":"http://terra-terrad:1317","tokenBridgeAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4","terraName":"localterra","terraChainId":"localterra","terraCoin":"uluna","terraGasPriceUrl":"http://terra-fcd:3060/v1/txs/gas_prices"},{"chainId":4,"chainName":"BSC","nativeCurrencySymbol":"BNB","nodeUrl":"http://eth-devnet2:8545","tokenBridgeAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16","wrappedAsset":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":18,"chainName":"Terra","isTerraClassic":false,"nativeCurrencySymbol":"LUNA","nodeUrl":"http://terra2-terrad:1317","tokenBridgeAddress":"terra1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrquka9l6","terraName":"localterra","terraChainId":"localterra","terraCoin":"uluna","terraGasPriceUrl":"http://terra2-fcd:3060/v1/txs/gas_prices"}]
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
PROM_PORT=8083
|
||||
READINESS_PORT=2000
|
||||
CLEAR_REDIS_ON_INIT=true
|
||||
DEMOTE_WORKING_ON_INIT=true
|
||||
LOG_LEVEL=debug
|
||||
SUPPORTED_TOKENS=[{"chainId":1,"address":"So11111111111111111111111111111111111111112"},{"chainId":1,"address":"2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"},{"chainId":2,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":2,"address":"0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A"},{"chainId":3,"address":"uluna"},{"chainId":4,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":18,"address":"uluna"}]
|
||||
PRIVATE_KEYS=[{"chainId":1,"privateKeys":[[14,173,153,4,176,224,201,111,32,237,183,185,159,247,22,161,89,84,215,209,212,137,10,92,157,49,29,192,101,164,152,70,87,65,8,174,214,157,175,126,98,90,54,24,100,177,247,77,19,112,47,44,165,109,233,102,14,86,109,29,134,145,132,141]]},{"chainId":2,"privateKeys":["0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"]},{"chainId":3,"privateKeys":["notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"]},{"chainId":4,"privateKeys":["0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"]},{"chainId":18,"privateKeys":["symbol force gallery make bulk round subway violin worry mixture penalty kingdom boring survey tool fringe patrol sausage hard admit remember broken alien absorb"]}]
|
|
@ -1,10 +0,0 @@
|
|||
SUPPORTED_CHAINS=[{"chainId":1,"chainName":"Solana","nodeUrl":"http://solana-devnet:8899","nativeCurrencySymbol":"SOL","tokenBridgeAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE","bridgeAddress":"Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","wrappedAsset":"So11111111111111111111111111111111111111112"},{"chainId":2,"chainName":"ETH","nativeCurrencySymbol":"ETH","nodeUrl":"http://eth-devnet:8545","tokenBridgeAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16","wrappedAsset":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":3,"chainName":"Terra Classic","isTerraClassic":true,"nativeCurrencySymbol":"LUNC","nodeUrl":"http://terra-terrad:1317","tokenBridgeAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4","terraName":"localterra","terraChainId":"localterra","terraCoin":"uluna","terraGasPriceUrl":"http://terra-fcd:3060/v1/txs/gas_prices"},{"chainId":4,"chainName":"BSC","nativeCurrencySymbol":"BNB","nodeUrl":"http://eth-devnet2:8545","tokenBridgeAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16","wrappedAsset":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":18,"chainName":"Terra","isTerraClassic":false,"nativeCurrencySymbol":"LUNA","nodeUrl":"http://terra2-terrad:1317","tokenBridgeAddress":"terra1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrquka9l6","terraName":"localterra","terraChainId":"localterra","terraCoin":"uluna","terraGasPriceUrl":"http://terra2-fcd:3060/v1/txs/gas_prices"}]
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
PROM_PORT=8084
|
||||
READINESS_PORT=2000
|
||||
CLEAR_REDIS_ON_INIT=true
|
||||
DEMOTE_WORKING_ON_INIT=true
|
||||
LOG_LEVEL=debug
|
||||
SUPPORTED_TOKENS=[{"chainId":1,"address":"So11111111111111111111111111111111111111112"},{"chainId":1,"address":"2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"},{"chainId":2,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":2,"address":"0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A"},{"chainId":3,"address":"uluna"},{"chainId":4,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"},{"chainId":18,"address":"uluna"}]
|
||||
PRIVATE_KEYS=[{"chainId":1,"privateKeys":[[14,173,153,4,176,224,201,111,32,237,183,185,159,247,22,161,89,84,215,209,212,137,10,92,157,49,29,192,101,164,152,70,87,65,8,174,214,157,175,126,98,90,54,24,100,177,247,77,19,112,47,44,165,109,233,102,14,86,109,29,134,145,132,141]]},{"chainId":2,"privateKeys":["0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"]},{"chainId":3,"privateKeys":["notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"]},{"chainId":4,"privateKeys":["0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"]},{"chainId":18,"privateKeys":["symbol force gallery make bulk round subway violin worry mixture penalty kingdom boring survey tool fringe patrol sausage hard admit remember broken alien absorb"]}]
|
|
@ -1,2 +0,0 @@
|
|||
/lib
|
||||
*.log
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "spy_relay",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/lib/main.js",
|
||||
"outFiles": ["${workspaceFolder}/lib/**/*.js"],
|
||||
"preLaunchTask": "npm: build",
|
||||
"outputCapture": "std"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
# syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2
|
||||
|
||||
FROM node:lts-alpine3.15@sha256:a2c7f8ebdec79619fba306cec38150db44a45b48380d09603d3602139c5a5f92 as base
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add python3 \
|
||||
make \
|
||||
g++
|
||||
|
||||
COPY package.json package.json
|
||||
COPY package-lock.json package-lock.json
|
||||
COPY tsconfig.json tsconfig.json
|
||||
COPY jestconfig.json jestconfig.json
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci
|
||||
|
||||
COPY src src
|
||||
RUN npm run build && npm prune --production
|
||||
|
||||
FROM base AS application
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/wormhole-foundation/wormhole/tree/main/relayer/spy_relayer#readme"
|
||||
|
||||
COPY --from=builder /app/node_modules node_modules
|
||||
COPY --from=builder /app/lib lib
|
||||
COPY .env.tilt.listener /app/
|
||||
COPY .env.tilt.relayer /app/
|
||||
COPY .env.tilt.wallet-monitor /app/
|
||||
|
||||
CMD [ "node", "lib/main.js" ]
|
||||
|
||||
#TODO don't hardcode for tilt but accept env file
|
||||
# RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
|
||||
# npm run tilt_relay
|
|
@ -1,151 +0,0 @@
|
|||
# Relayer
|
||||
|
||||
The wormhole relayer is designed to answer one main question:
|
||||
|
||||
Q: How do you transfer to a new wallet in a destination chain when you lack native tokens for paying gas fees?
|
||||
A: You pay a little bit more on the source chain and use that to pay gas fees on the destination chain.
|
||||
|
||||
It was originally designed for payload version 1 token transfers, but should be extensible to other payload types as well.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Component | Description |
|
||||
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Guardian Spy | Connects to the wormhole p2p network and publishes all VAAs to a websocket |
|
||||
| Spy Listener | Filters VAAs from the Spy and adds them to the incoming queue in Redis |
|
||||
| REST Listener | Accepts HTTP requests to relay VAAs and writes them to the incoming queue in Redis |
|
||||
| Redis | A durable queue for storing VAAs before they are relayed |
|
||||
| Relayer | Scans the Redis incoming queue and moves acceptable VAAs to the working queue. It then completes the transfer and pays gas fees on the destination chain. |
|
||||
| Wallet Monitor | Presents a prometheus endpoint for monitoring wallet balances of native tokens (for paying gas fees) and non-native tokens as relayer profit |
|
||||
|
||||
If Redis is temporarily down, the Listener will queue outstanding transactions in memory. When Redis comes back online, the Listener writes them all to Redis.
|
||||
|
||||
### Architecture Diagram
|
||||
|
||||
This is a rough diagram of how the components fit together:
|
||||
|
||||
┌────────────────────────────────────────┐
|
||||
│ Wormhole Guardian Peer to Peer Network │
|
||||
└───────────────────┬────────────────────┘
|
||||
│
|
||||
┌──────▼───────┐
|
||||
│ Guardian Spy │
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌──────▼───────┐
|
||||
│ Spy Listener │
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌───▼───┐ ┌───────────────┐
|
||||
│ Redis │◄───┤ REST Listener │
|
||||
└───┬───┘ └───────────────┘
|
||||
│
|
||||
┌────▼────┐
|
||||
│ Relayer │
|
||||
└─────────┘
|
||||
│
|
||||
┌────────▼───────┐
|
||||
│ Wallet Monitor │
|
||||
└────────────────┘
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Listener
|
||||
|
||||
These are for configuring the spy and rest listener. See [.env.tilt.listener](.env.tilt.listener) for examples:
|
||||
|
||||
| Name | Description |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `SPY_SERVICE_HOST` | host & port string to connect to the spy |
|
||||
| `SPY_SERVICE_FILTERS` | Addresses to monitor (Wormhole core bridge contract addresses) array of ["chainId","emitterAddress"]. Emitter addresses are native strings. |
|
||||
| `REDIS_HOST` | Redis host / ip to connect to |
|
||||
| `REDIS_PORT` | Redis port |
|
||||
| `REST_PORT` | Rest listener port to listen on. |
|
||||
| `READINESS_PORT` | Kubernetes readiness probe port to listen on. |
|
||||
| `LOG_LEVEL` | log level, such as debug |
|
||||
| `SUPPORTED_TOKENS` | Origin assets that will attempt to be relayed. Array of ["chainId","address"], address should be a native string. |
|
||||
|
||||
### Relayer
|
||||
|
||||
These are for configuring the actual relayer. See [.env.tilt.relayer](.env.tilt.relayer) for examples:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `SUPPORTED_CHAINS` | The configuration for each chain which will be relayed. See [chainConfigs.example.json](src/chainConfigs.example.json) for the format. Of note, `walletPrivateKey` is an array, and a separate worker will be spun up for every private key provided. |
|
||||
| `REDIS_HOST` | host of the redis service, should be the same as in the spy_listener |
|
||||
| `REDIS_PORT` | port for redis to connect to |
|
||||
| `PROM_PORT` | port where prometheus monitoring will listen |
|
||||
| `READINESS_PORT` | port for kubernetes readiness probe |
|
||||
| `CLEAR_REDIS_ON_INIT` | boolean, if `true` the relayer will clear the INCOMING and WORKING Redis tables before it starts up. |
|
||||
| `DEMOTE_WORKING_ON_INIT` | boolean, if `true` the relayer will move everything from the WORKING Redis table to the INCOMING one. |
|
||||
| `LOG_LEVEL` | log level, debug or info |
|
||||
|
||||
## Building
|
||||
|
||||
### Building the Spy
|
||||
|
||||
To build the guardiand / spy container from source:
|
||||
|
||||
```bash
|
||||
cd node
|
||||
docker build -f Dockerfile -t guardian .
|
||||
```
|
||||
|
||||
### Building the Relayer application
|
||||
|
||||
Build the relayer for non-containerized testing:
|
||||
|
||||
```bash
|
||||
cd relayer/spy_relayer
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Running the Whole Stack For Testing
|
||||
|
||||
This config is mostly for development.
|
||||
|
||||
### Run Redis
|
||||
|
||||
Start a redis container:
|
||||
|
||||
```bash
|
||||
docker run --rm -p6379:6379 --name redis-docker -d redis
|
||||
```
|
||||
|
||||
### Run the Guardian Spy
|
||||
|
||||
The spy connects to the wormhole guardian peer to peer network and listens for new VAAs. It publishes those via a socket and websocket that the listener subscribes to. If you want to run the spy built from source, change `ghcr.io/wormhole-foundation/guardiand:latest` to `guardian` after building the `guardian` image.
|
||||
|
||||
Start the spy against the testnet wormhole guardian:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--platform=linux/amd64 \
|
||||
-p 7073:7073 \
|
||||
--entrypoint /guardiand \
|
||||
ghcr.io/wormhole-foundation/guardiand:latest \
|
||||
spy --nodeKey /node.key --spyRPC "[::]:7073" --network /wormhole/testnet/2/1 --bootstrap /dns4/wormhole-testnet-v2-bootstrap.certus.one/udp/8999/quic/p2p/12D3KooWAkB9ynDur1Jtoa97LBUp8RXdhzS5uHgAfdTquJbrbN7i
|
||||
```
|
||||
|
||||
To run the spy against mainnet:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--platform=linux/amd64 \
|
||||
-p 7073:7073 \
|
||||
--entrypoint /guardiand \
|
||||
ghcr.io/wormhole-foundation/guardiand:latest \
|
||||
spy --nodeKey /node.key --spyRPC "[::]:7073" --network /wormhole/mainnet/2 --bootstrap /dns4/wormhole-mainnet-v2-bootstrap.certus.one/udp/8999/quic/p2p/12D3KooWQp644DK27fd3d4Km3jr7gHiuJJ5ZGmy8hH4py7fP4FP7,/dns4/wormhole-v2-mainnet-bootstrap.xlabs.xyz/udp/8999/quic/p2p/12D3KooWNQ9tVrcb64tw6bNs2CaNrUGPM7yRrKvBBheQ5yCyPHKC
|
||||
```
|
||||
|
||||
### Run The Apps
|
||||
|
||||
This runs the Spy Listener, REST Listener, Relayer, and Wallet Monitor all in a single process for development and testing purposes:
|
||||
|
||||
Start the application:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run spy_relay
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
SPY_SERVICE_HOST= change me (spyhost:port)
|
||||
SPY_SERVICE_FILTERS= paste from emitterAddresses.json
|
||||
SPY_NUM_WORKERS=5
|
||||
|
||||
REDIS_HOST= change me
|
||||
REDIS_PORT= change me
|
||||
|
||||
REST_PORT=4201
|
||||
PROM_PORT=8082
|
||||
READINESS_PORT=2000
|
||||
|
||||
LOG_LEVEL=debug
|
||||
SUPPORTED_TOKENS= paste from supportedTokens.json
|
|
@ -1,10 +0,0 @@
|
|||
SUPPORTED_CHAINS= paste from supportedChains.json
|
||||
REDIS_HOST= change me
|
||||
REDIS_PORT= change me
|
||||
PROM_PORT=8083
|
||||
READINESS_PORT=2000
|
||||
CLEAR_REDIS_ON_INIT=false
|
||||
DEMOTE_WORKING_ON_INIT=true
|
||||
LOG_LEVEL=debug
|
||||
SUPPORTED_TOKENS= paste from supportedTokens.json. This must be the same as in the .env.listener file.
|
||||
PRIVATE_KEYS= paste from privateKeys.json
|
|
@ -1,10 +0,0 @@
|
|||
SUPPORTED_CHAINS= paste from supportedChains.json
|
||||
REDIS_HOST= change me
|
||||
REDIS_PORT= change me
|
||||
PROM_PORT=8084
|
||||
READINESS_PORT=2000
|
||||
CLEAR_REDIS_ON_INIT=false
|
||||
DEMOTE_WORKING_ON_INIT=true
|
||||
LOG_LEVEL=debug
|
||||
SUPPORTED_TOKENS= paste from supportedTokens.json. This must be the same as in the .env.listener file.
|
||||
PRIVATE_KEYS= paste from privateKeys.json
|
|
@ -1,39 +0,0 @@
|
|||
To utilize:
|
||||
|
||||
- Alter the information supportedChains.json marked as CHANGE ME.
|
||||
- Modify supportedTokens.json as needed
|
||||
- Modify privateKeys.json as needed
|
||||
- Remove newlines from supportedChains.json
|
||||
- Remove newlines from emitterAddress.json
|
||||
- Remove newlines from supportedTokens.json
|
||||
- Remove newlines from privateKeys.json
|
||||
- Add the required information to .env.listener & .env.relayer
|
||||
|
||||
Useful addresses:
|
||||
|
||||
WSOL:
|
||||
So11111111111111111111111111111111111111112
|
||||
|
||||
WETH:
|
||||
0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
|
||||
|
||||
UST:
|
||||
uusd
|
||||
|
||||
LUNA:
|
||||
uluna
|
||||
|
||||
WBNB:
|
||||
0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
|
||||
|
||||
WMATIC:
|
||||
0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270
|
||||
|
||||
WAVAX:
|
||||
0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7
|
||||
|
||||
WROSE:
|
||||
0x21C718C22D52d0F3a789b752D4c2fD5908a8A733
|
||||
|
||||
WFTM:
|
||||
0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83
|
|
@ -1,16 +0,0 @@
|
|||
[
|
||||
{"chainId":1,"emitterAddress":"wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb"},
|
||||
{"chainId":2,"emitterAddress":"0x3ee18B2214AFF97000D974cf647E7C347E8fa585"},
|
||||
{"chainId":3,"emitterAddress":"terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf"},
|
||||
{"chainId":4,"emitterAddress":"0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7"},
|
||||
{"chainId":5,"emitterAddress":"0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE"},
|
||||
{"chainId":6,"emitterAddress":"0x0e082F06FF657D94310cB8cE8B0D9a04541d8052"},
|
||||
{"chainId":7,"emitterAddress":"0x5848C791e09901b40A9Ef749f2a6735b418d7564"},
|
||||
{"chainId":9,"emitterAddress":"0x51b5123a7b0f9b2ba265f9c4c8de7d78d52f510f"},
|
||||
{"chainId":10,"emitterAddress":"0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2"},
|
||||
{"chainId":11,"emitterAddress":"0xae9d7fe007b3327AA64A32824Aaac52C42a6E624"},
|
||||
{"chainId":12,"emitterAddress":"0xae9d7fe007b3327AA64A32824Aaac52C42a6E624"},
|
||||
{"chainId":13,"emitterAddress":"0x5b08ac39EAED75c0439FC750d9FE7E1F9dD0193F"},
|
||||
{"chainId":14,"emitterAddress":"0x796Dff6D74F3E27060B71255Fe517BFb23C93eed"},
|
||||
{"chainId":18,"emitterAddress":"terra153366q50k7t8nn7gec00hg66crnhkdggpgdtaxltaq6xrutkkz3s992fw9"}
|
||||
]
|
|
@ -1,32 +0,0 @@
|
|||
[
|
||||
{
|
||||
"chainId": 1,
|
||||
"privateKeys": [
|
||||
[
|
||||
"change", "me", 153, 4, 176, 224, 201, 111, 32, 237, 183, 185, 159, 247, 22,
|
||||
161, 89, 84, 215, 209, 212, 137, 10, 92, 157, 49, 29, 192, 101, 164,
|
||||
152, 70, 87, 65, 8, 174, 214, 157, 175, 126, 98, 90, 54, 24, 100, 177,
|
||||
247, 77, 19, 112, 47, 44, 165, 109, 233, 102, 14, 86, 109, 29, 134, 145,
|
||||
132, 141
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": 2,
|
||||
"privateKeys": [
|
||||
"0xCHANGEMEac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": 3,
|
||||
"privateKeys": [
|
||||
"CHANGE ME worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": 4,
|
||||
"privateKeys": [
|
||||
"0xCHANGEME3ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -1,107 +0,0 @@
|
|||
[
|
||||
{
|
||||
"chainId": 1,
|
||||
"chainName": "Solana",
|
||||
"nativeCurrencySymbol": "SOL",
|
||||
"nodeUrl": "CHANGE ME",
|
||||
"tokenBridgeAddress": "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb",
|
||||
"bridgeAddress": "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth",
|
||||
"wrappedAsset": "So11111111111111111111111111111111111111112"
|
||||
},
|
||||
{
|
||||
"chainId": 2,
|
||||
"chainName": "Ethereum",
|
||||
"nativeCurrencySymbol": "ETH",
|
||||
"nodeUrl": "CHANGE ME",
|
||||
"tokenBridgeAddress": "0x3ee18B2214AFF97000D974cf647E7C347E8fa585",
|
||||
"wrappedAsset": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||
},
|
||||
{
|
||||
"chainId": 3,
|
||||
"chainName": "Terra Classic",
|
||||
"nativeCurrencySymbol": "LUNC",
|
||||
"nodeUrl": "https://columbus-fcd.terra.dev OR SOMETHING ELSE... ALSO UPDATE GAS PRICE URL",
|
||||
"tokenBridgeAddress": "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf",
|
||||
"terraName": "mainnet",
|
||||
"terraChainId": "columbus-5",
|
||||
"terraCoin": "uluna",
|
||||
"terraGasPriceUrl": "https://columbus-fcd.terra.dev/v1/txs/gas_prices <- SHOULD BE SAME AS NODE URL",
|
||||
"isTerraClassic": true
|
||||
},
|
||||
{
|
||||
"chainId": 4,
|
||||
"chainName": "Binance Smart Chain",
|
||||
"nativeCurrencySymbol": "BNB",
|
||||
"nodeUrl": "CHANGE ME",
|
||||
"tokenBridgeAddress": "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7",
|
||||
"wrappedAsset": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"
|
||||
},
|
||||
{
|
||||
"chainId": 5,
|
||||
"chainName": "Polygon",
|
||||
"nativeCurrencySymbol": "MATIC",
|
||||
"nodeUrl": "CHANGE ME",
|
||||
"tokenBridgeAddress": "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE",
|
||||
"wrappedAsset": "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270"
|
||||
},
|
||||
{
|
||||
"chainId": 6,
|
||||
"chainName": "Avalanche",
|
||||
"nativeCurrencySymbol": "AVAX",
|
||||
"nodeUrl": "CHANGE ME",
|
||||
"tokenBridgeAddress": "0x0e082F06FF657D94310cB8cE8B0D9a04541d8052",
|
||||
"wrappedAsset": "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7"
|
||||
},
|
||||
{
|
||||
"chainId": 7,
|
||||
"chainName": "Oasis",
|
||||
"nativeCurrencySymbol": "ROSE",
|
||||
"nodeUrl": "CHANGE ME",
|
||||
"tokenBridgeAddress": "0x5848C791e09901b40A9Ef749f2a6735b418d7564",
|
||||
"wrappedAsset": "0x21C718C22D52d0F3a789b752D4c2fD5908a8A733"
|
||||
},
|
||||
{
|
||||
"chainId": 9,
|
||||
"chainName": "Aurora",
|
||||
"nativeCurrencySymbol": "ETH",
|
||||
"nodeUrl": "https://mainnet.aurora.dev",
|
||||
"tokenBridgeAddress": "0x51b5123a7b0f9b2ba265f9c4c8de7d78d52f510f",
|
||||
"wrappedAsset": "0xC9BdeEd33CD01541e1eeD10f90519d2C06Fe3feB"
|
||||
},
|
||||
{
|
||||
"chainId": 10,
|
||||
"chainName": "Fantom",
|
||||
"nativeCurrencySymbol": "FTM",
|
||||
"nodeUrl": "CHANGE ME",
|
||||
"tokenBridgeAddress": "0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2",
|
||||
"wrappedAsset": "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83"
|
||||
},
|
||||
{
|
||||
"chainId": 13,
|
||||
"chainName": "Klaytn",
|
||||
"nativeCurrencySymbol": "KLAY",
|
||||
"nodeUrl": "https://public-node-api.klaytnapi.com/v1/cypress",
|
||||
"tokenBridgeAddress": "0x5b08ac39EAED75c0439FC750d9FE7E1F9dD0193F",
|
||||
"wrappedAsset": "0xe4f05a66ec68b54a58b17c22107b02e0232cc817"
|
||||
},
|
||||
{
|
||||
"chainId": 14,
|
||||
"chainName": "Celo",
|
||||
"nativeCurrencySymbol": "CELO",
|
||||
"nodeUrl": "https://forno.celo.org",
|
||||
"tokenBridgeAddress": "0x796Dff6D74F3E27060B71255Fe517BFb23C93eed",
|
||||
"wrappedAsset": "0x0000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"chainId": 18,
|
||||
"chainName": "Terra",
|
||||
"nativeCurrencySymbol": "LUNA",
|
||||
"nodeUrl": "https://phoenix-fcd.terra.dev OR SOMETHING ELSE... ALSO UPDATE GAS PRICE URL",
|
||||
"tokenBridgeAddress": "terra153366q50k7t8nn7gec00hg66crnhkdggpgdtaxltaq6xrutkkz3s992fw9",
|
||||
"terraName": "mainnet",
|
||||
"terraChainId": "phoenix-1",
|
||||
"terraCoin": "uluna",
|
||||
"terraGasPriceUrl": "https://phoenix-fcd.terra.dev/v1/txs/gas_prices <- SHOULD BE SAME AS NODE URL",
|
||||
"isTerraClassic": false
|
||||
}
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
[
|
||||
{ "chainId": 1, "address": "So11111111111111111111111111111111111111112" },
|
||||
{ "chainId": 2, "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" },
|
||||
{ "chainId": 3, "address": "uluna" },
|
||||
{ "chainId": 3, "address": "uusd" },
|
||||
{ "chainId": 4, "address": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" },
|
||||
{ "chainId": 5, "address": "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270" },
|
||||
{ "chainId": 6, "address": "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7" },
|
||||
{ "chainId": 7, "address": "0x21C718C22D52d0F3a789b752D4c2fD5908a8A733" },
|
||||
{ "chainId": 9, "address": "0xC9BdeEd33CD01541e1eeD10f90519d2C06Fe3feB" },
|
||||
{ "chainId": 10, "address": "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83" },
|
||||
{ "chainId": 13, "address": "0xe4f05a66ec68b54a58b17c22107b02e0232cc817" },
|
||||
{ "chainId": 14, "address": "0x471ece3750da237f93b8e339c536989b8978a438" },
|
||||
{ "chainId": 2, "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" },
|
||||
{ "chainId": 2, "address": "0xdac17f958d2ee523a2206206994597c13d831ec7" },
|
||||
{ "chainId": 18, "address": "uluna" }
|
||||
]
|
|
@ -1,47 +0,0 @@
|
|||
## Docker Images
|
||||
|
||||
VAA_Listener
|
||||
Redis
|
||||
Relayer
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Guardian Spy
|
||||
- Blockchain Nodes for all supported target chains
|
||||
|
||||
## High Level Workflow:
|
||||
|
||||
The VAA_Listener listens for Token Bridge SignedVAAs coming from both the guardian network (via a guardian spy), and end users (via a REST interface).
|
||||
|
||||
The VAA_Listener then passes this SignedVAA into a validate function, which determines if this VAA should be processed. If so, it enqueues the VAA in redis to be processed by the relayer.
|
||||
|
||||
Validation criteria:
|
||||
|
||||
- VAA must be token bridge signedVAA of type payload 1.
|
||||
- VAA be for a supported target chain & origin asset.
|
||||
- VAA must have a sufficiently high 'fee' field on it.
|
||||
- VAA must not already be in the 'incoming', 'in-work', or 'pending confirmation' redis tables. (Optionally, also a max-retries exceeded table?)
|
||||
- VAA must not be already redeemed.
|
||||
|
||||
# Redis
|
||||
|
||||
Four tables:
|
||||
|
||||
- Incoming: These are requests which have been queued by the listener, but have not yet been attempted by the relayer.
|
||||
- In-Work: These are requests which have been popped off the 'incoming' stack, but have not yet been successfully submitted on chain.
|
||||
- Pending Confirmation: These are requests which have been successfully submitted on chain, and are waiting for a finality check to ensure they were not rolled back.
|
||||
- Failed: These are requests which were removed from the In-Work table due to having exceeded their max number of retries.
|
||||
|
||||
All requests enter via the 'Incoming' table, and should eventually either be 'purged' once they successfully exit the Pending Confirmation table, or end in the "Failed" table. For data retention purposes, it may be worthwhile to have a "Completed" table, however, logging should be sufficient for this.
|
||||
|
||||
# Relayer
|
||||
|
||||
The relayer is responsible for monitoring redis and submitting transaction on chain.
|
||||
|
||||
The relayer spawns a worker for each combination of {targetChain + privateKey}, such that no two schedulers should collide on-chain.
|
||||
|
||||
Each worker perpetually attempts to submit items in the 'In-Work' table which are assigned to them. When they successfully process an In-Work item, they move it to the Pending Confirmation table. If they are not successful, they increment the failure-count on the In-Work item. If the failure-count exceeds MAX_RETRIES, the In-Work item is moved to the 'Failed' table.
|
||||
|
||||
If there are no eligible items in the In-Work table, the worker will scan the Incoming table, and move the Incoming item into the 'In-Work' table under their name. Workers are identified by a string which is their target chain + the public key of their wallet.
|
||||
|
||||
Prior to submitting a signedVAA, relayers should check that the VAA has not been redeemed, as other processes may 'scoop' a VAA.
|
|
@ -1,30 +0,0 @@
|
|||
version: "3.5"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7.0
|
||||
command: redis-server --save 60 1 --loglevel warning
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
guardian-spy:
|
||||
image: ghcr.io/wormhole-foundation/guardiand:main
|
||||
entrypoint: /guardiand
|
||||
command: ${GUARDIAN_SPY_COMMAND:-spy --nodeKey /node.key --spyRPC "[::]:7073" --network /wormhole/testnet/2/1 --bootstrap /dns4/wormhole-testnet-v2-bootstrap.certus.one/udp/8999/quic/p2p/12D3KooWBY9ty9CXLBXGQzMuqkziLntsVcyz4pk1zWaJRvJn6Mmt}
|
||||
|
||||
spy-relayer:
|
||||
build:
|
||||
context: .
|
||||
target: application
|
||||
image: wormhole-relayer:main
|
||||
command: ${SPY_RELAYER_COMMAND:-npm run spy_relay}
|
||||
environment:
|
||||
- SPY_RELAY_CONFIG=/app/.env.docker.sample
|
||||
ports:
|
||||
- 8083:8083
|
||||
- 4201:4201
|
||||
volumes:
|
||||
- .env.docker.sample:/app/.env.docker.sample
|
||||
|
||||
volumes:
|
||||
redis-data:
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"transform": {
|
||||
"^.+\\.(t|j)sx?$": "ts-jest"
|
||||
},
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||
"transformIgnorePatterns": ["/node_modules/"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,51 +0,0 @@
|
|||
{
|
||||
"name": "@wormhole-foundation/spy-relay",
|
||||
"version": "0.0.1",
|
||||
"description": "Spy listener and relayer",
|
||||
"main": "spy_relay.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"spy_relay": "node lib/main.js",
|
||||
"tilt_listener": "SPY_RELAY_CONFIG=.env.tilt.listener node lib/main.js --listen_only",
|
||||
"tilt_relayer": "SPY_RELAY_CONFIG=.env.tilt.relayer node lib/main.js --relay_only",
|
||||
"tilt_wallet_monitor": "SPY_RELAY_CONFIG=.env.tilt.wallet-monitor node lib/main.js --wallet_monitor_only",
|
||||
"listen_only": "node lib/main.js --listen_only",
|
||||
"relay_only": "node lib/main.js --relay_only",
|
||||
"wallet_monitor_only": "node lib/main.js --wallet_monitor_only",
|
||||
"test": "jest --config jestconfig.json --verbose"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": "^16.6.1",
|
||||
"axios": "^0.24.0",
|
||||
"esm": "^3.2.25",
|
||||
"ethers": "5.4.4",
|
||||
"jest": "^27.3.1",
|
||||
"prettier": "^2.3.2",
|
||||
"ts-jest": "^27.0.7",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@celo-tools/celo-ethers-wrapper": "^0.1.0",
|
||||
"@certusone/wormhole-sdk": "^0.5.0",
|
||||
"@certusone/wormhole-spydk": "^0.0.1",
|
||||
"@solana/spl-token": "^0.1.8",
|
||||
"@solana/web3.js": "^1.24.0",
|
||||
"@terra-money/wallet-provider": "^3.8.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"async-mutex": "^0.3.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"prom-client": "^14.0.1",
|
||||
"redis": "^4.0.1",
|
||||
"winston": "^3.3.3"
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import { describe, expect, it } from "@jest/globals";
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
|
||||
// see devnet.md
|
||||
export const ETH_NODE_URL = "ws://localhost:8545";
|
||||
export const ETH_PRIVATE_KEY =
|
||||
"0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d";
|
||||
export const ETH_PUBLIC_KEY = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
|
||||
export const ETH_CORE_BRIDGE_ADDRESS =
|
||||
"0xC89Ce4735882C9F0f0FE26686c53074E09B0D550";
|
||||
export const ETH_TOKEN_BRIDGE_ADDRESS =
|
||||
"0x0290FB167208Af455bB137780163b7B7a9a10C16";
|
||||
export const SOLANA_HOST = "http://localhost:8899";
|
||||
export const SOLANA_PRIVATE_KEY = new Uint8Array([
|
||||
14, 173, 153, 4, 176, 224, 201, 111, 32, 237, 183, 185, 159, 247, 22, 161, 89,
|
||||
84, 215, 209, 212, 137, 10, 92, 157, 49, 29, 192, 101, 164, 152, 70, 87, 65,
|
||||
8, 174, 214, 157, 175, 126, 98, 90, 54, 24, 100, 177, 247, 77, 19, 112, 47,
|
||||
44, 165, 109, 233, 102, 14, 86, 109, 29, 134, 145, 132, 141,
|
||||
]);
|
||||
export const SOLANA_CORE_BRIDGE_ADDRESS =
|
||||
"Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
|
||||
export const SOLANA_TOKEN_BRIDGE_ADDRESS =
|
||||
"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE";
|
||||
export const TERRA_NODE_URL = "http://localhost:1317";
|
||||
export const TERRA2_NODE_URL = "http://localhost:1318";
|
||||
export const TERRA_CHAIN_ID = "localterra";
|
||||
export const TERRA_GAS_PRICES_URL = "http://localhost:3060/v1/txs/gas_prices";
|
||||
export const TERRA2_GAS_PRICES_URL = "http://localhost:3061/v1/txs/gas_prices";
|
||||
export const TERRA_CORE_BRIDGE_ADDRESS =
|
||||
"terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
|
||||
export const TERRA_TOKEN_BRIDGE_ADDRESS =
|
||||
"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4";
|
||||
export const TERRA2_TOKEN_BRIDGE_ADDRESS =
|
||||
"terra1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrquka9l6";
|
||||
export const TERRA_PRIVATE_KEY =
|
||||
"notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius";
|
||||
export const TEST_ERC20 = "0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A";
|
||||
export const TEST_SOLANA_TOKEN = "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ";
|
||||
export const WORMHOLE_RPC_HOSTS = ["http://localhost:7071"];
|
||||
|
||||
export const SPY_RELAY_URL = "http://localhost:4201";
|
||||
|
||||
describe("consts should exist", () => {
|
||||
it("has Solana test token", () => {
|
||||
expect.assertions(1);
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
return expect(
|
||||
connection.getAccountInfo(new PublicKey(TEST_SOLANA_TOKEN))
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,830 +0,0 @@
|
|||
import {
|
||||
approveEth,
|
||||
attestFromEth,
|
||||
attestFromSolana,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
createWrappedOnEth,
|
||||
createWrappedOnSolana,
|
||||
createWrappedOnTerra,
|
||||
getEmitterAddressEth,
|
||||
getEmitterAddressSolana,
|
||||
getForeignAssetSolana,
|
||||
getIsTransferCompletedEth,
|
||||
getIsTransferCompletedSolana,
|
||||
getIsTransferCompletedTerra,
|
||||
hexToUint8Array,
|
||||
nativeToHexString,
|
||||
postVaaSolana,
|
||||
parseSequenceFromLogEth,
|
||||
parseSequenceFromLogSolana,
|
||||
transferFromEth,
|
||||
transferFromSolana,
|
||||
CHAIN_ID_TERRA2,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
|
||||
import getSignedVAAWithRetry from "@certusone/wormhole-sdk/lib/cjs/rpc/getSignedVAAWithRetry";
|
||||
import { setDefaultWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
|
||||
|
||||
import { parseUnits } from "@ethersproject/units";
|
||||
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
|
||||
import { describe, expect, jest, test } from "@jest/globals";
|
||||
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { LCDClient, MnemonicKey } from "@terra-money/terra.js";
|
||||
import axios from "axios";
|
||||
import {
|
||||
ETH_CORE_BRIDGE_ADDRESS,
|
||||
ETH_NODE_URL,
|
||||
ETH_PRIVATE_KEY,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_CORE_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOLANA_PRIVATE_KEY,
|
||||
SOLANA_TOKEN_BRIDGE_ADDRESS,
|
||||
SPY_RELAY_URL,
|
||||
TERRA2_GAS_PRICES_URL,
|
||||
TERRA2_NODE_URL,
|
||||
TERRA2_TOKEN_BRIDGE_ADDRESS,
|
||||
TERRA_CHAIN_ID,
|
||||
TERRA_GAS_PRICES_URL,
|
||||
TERRA_NODE_URL,
|
||||
TERRA_PRIVATE_KEY,
|
||||
TERRA_TOKEN_BRIDGE_ADDRESS,
|
||||
TEST_ERC20,
|
||||
TEST_SOLANA_TOKEN,
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
} from "./consts";
|
||||
|
||||
import { sleep } from "../helpers/utils";
|
||||
|
||||
setDefaultWasm("node");
|
||||
|
||||
jest.setTimeout(60000);
|
||||
|
||||
test("Verify Spy Relay is running", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
console.log(
|
||||
"Sending query to spy relay to see if it's running, query: [%s]",
|
||||
SPY_RELAY_URL
|
||||
);
|
||||
|
||||
const result = await axios.get(SPY_RELAY_URL);
|
||||
|
||||
expect(result).toHaveProperty("status");
|
||||
expect(result.status).toBe(200);
|
||||
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error("Spy Relay does not appear to be running!");
|
||||
console.error(e);
|
||||
done("Spy Relay does not appear to be running!");
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
let sequence: string;
|
||||
let emitterAddress: string;
|
||||
let transferSignedVAA: Uint8Array;
|
||||
|
||||
describe("Solana to Ethereum", () => {
|
||||
test("Attest Solana SPL to Ethereum", (done) => {
|
||||
(async () => {
|
||||
console.log("Attest Solana SPL to Ethereum");
|
||||
try {
|
||||
// create a keypair for Solana
|
||||
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
|
||||
const payerAddress = keypair.publicKey.toString();
|
||||
// attest the test token
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const transaction = await attestFromSolana(
|
||||
connection,
|
||||
SOLANA_CORE_BRIDGE_ADDRESS,
|
||||
SOLANA_TOKEN_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
TEST_SOLANA_TOKEN
|
||||
);
|
||||
// sign, send, and confirm transaction
|
||||
transaction.partialSign(keypair);
|
||||
const txid = await connection.sendRawTransaction(
|
||||
transaction.serialize()
|
||||
);
|
||||
await connection.confirmTransaction(txid);
|
||||
const info = await connection.getTransaction(txid);
|
||||
if (!info) {
|
||||
throw new Error(
|
||||
"An error occurred while fetching the transaction info"
|
||||
);
|
||||
}
|
||||
// get the sequence from the logs (needed to fetch the vaa)
|
||||
const sequence = parseSequenceFromLogSolana(info);
|
||||
emitterAddress = await getEmitterAddressSolana(
|
||||
SOLANA_TOKEN_BRIDGE_ADDRESS
|
||||
);
|
||||
// poll until the guardian(s) witness and sign the vaa
|
||||
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_SOLANA,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
}
|
||||
);
|
||||
// create a signer for Eth
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
|
||||
try {
|
||||
await createWrappedOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
|
||||
} catch (e) {
|
||||
// this could fail because the token is already attested (in an unclean env)
|
||||
}
|
||||
provider.destroy();
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done(
|
||||
"An error occurred while trying to attest from Solana to Ethereum"
|
||||
);
|
||||
}
|
||||
})();
|
||||
});
|
||||
// TODO: it is attested
|
||||
test("Send Solana SPL to Ethereum", (done) => {
|
||||
(async () => {
|
||||
console.log("Send Solana SPL to Ethereum");
|
||||
try {
|
||||
// create a signer for Eth
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
|
||||
const targetAddress = await signer.getAddress();
|
||||
// create a keypair for Solana
|
||||
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
|
||||
const payerAddress = keypair.publicKey.toString();
|
||||
// find the associated token account
|
||||
const fromAddress = (
|
||||
await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
new PublicKey(TEST_SOLANA_TOKEN),
|
||||
keypair.publicKey
|
||||
)
|
||||
).toString();
|
||||
// transfer the test token
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const amount = parseUnits("1", 9).toBigInt();
|
||||
const fee = parseUnits("1", 3).toBigInt();
|
||||
const transaction = await transferFromSolana(
|
||||
connection,
|
||||
SOLANA_CORE_BRIDGE_ADDRESS,
|
||||
SOLANA_TOKEN_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
fromAddress,
|
||||
TEST_SOLANA_TOKEN,
|
||||
amount + fee,
|
||||
hexToUint8Array(nativeToHexString(targetAddress, CHAIN_ID_ETH) || ""),
|
||||
CHAIN_ID_ETH,
|
||||
Buffer.from(TEST_SOLANA_TOKEN),
|
||||
CHAIN_ID_SOLANA,
|
||||
undefined,
|
||||
fee
|
||||
);
|
||||
// sign, send, and confirm transaction
|
||||
console.log("Sending transaction.");
|
||||
transaction.partialSign(keypair);
|
||||
const txid = await connection.sendRawTransaction(
|
||||
transaction.serialize()
|
||||
);
|
||||
console.log("Confirming transaction.");
|
||||
await connection.confirmTransaction(txid);
|
||||
const info = await connection.getTransaction(txid);
|
||||
if (!info) {
|
||||
throw new Error(
|
||||
"An error occurred while fetching the transaction info"
|
||||
);
|
||||
}
|
||||
// get the sequence from the logs (needed to fetch the vaa)
|
||||
console.log("Parsing sequence number from log.");
|
||||
sequence = parseSequenceFromLogSolana(info);
|
||||
const emitterAddress = await getEmitterAddressSolana(
|
||||
SOLANA_TOKEN_BRIDGE_ADDRESS
|
||||
);
|
||||
// poll until the guardian(s) witness and sign the vaa
|
||||
console.log("Waiting on signed vaa, sequence %d", sequence);
|
||||
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_SOLANA,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
}
|
||||
);
|
||||
console.log("Got signed vaa: ", signedVAA);
|
||||
transferSignedVAA = signedVAA;
|
||||
provider.destroy();
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done("An error occurred while trying to send from Solana to Ethereum");
|
||||
}
|
||||
})();
|
||||
});
|
||||
test("Spy Relay redeemed on Eth", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
let success: boolean = false;
|
||||
for (let count = 0; count < 5 && !success; ++count) {
|
||||
console.log(
|
||||
"sleeping before querying spy relay",
|
||||
new Date().toLocaleString()
|
||||
);
|
||||
await sleep(5000);
|
||||
success = await getIsTransferCompletedEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
provider,
|
||||
transferSignedVAA
|
||||
);
|
||||
console.log(
|
||||
"getIsTransferCompletedEth returned %d, count is %d",
|
||||
success,
|
||||
count
|
||||
);
|
||||
}
|
||||
expect(success).toBe(true);
|
||||
provider.destroy();
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done("An error occurred while trying to redeem on Eth");
|
||||
}
|
||||
})();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ethereum to Solana", () => {
|
||||
test("Attest Ethereum ERC-20 to Solana", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
// create a signer for Eth
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
|
||||
// attest the test token
|
||||
const receipt = await attestFromEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
signer,
|
||||
TEST_ERC20
|
||||
);
|
||||
// get the sequence from the logs (needed to fetch the vaa)
|
||||
const sequence = parseSequenceFromLogEth(
|
||||
receipt,
|
||||
ETH_CORE_BRIDGE_ADDRESS
|
||||
);
|
||||
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
||||
// poll until the guardian(s) witness and sign the vaa
|
||||
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
}
|
||||
);
|
||||
// create a keypair for Solana
|
||||
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
|
||||
const payerAddress = keypair.publicKey.toString();
|
||||
// post vaa to Solana
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
await postVaaSolana(
|
||||
connection,
|
||||
async (transaction) => {
|
||||
transaction.partialSign(keypair);
|
||||
return transaction;
|
||||
},
|
||||
SOLANA_CORE_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
Buffer.from(signedVAA)
|
||||
);
|
||||
// create wormhole wrapped token (mint and metadata) on solana
|
||||
const transaction = await createWrappedOnSolana(
|
||||
connection,
|
||||
SOLANA_CORE_BRIDGE_ADDRESS,
|
||||
SOLANA_TOKEN_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
signedVAA
|
||||
);
|
||||
// sign, send, and confirm transaction
|
||||
try {
|
||||
transaction.partialSign(keypair);
|
||||
const txid = await connection.sendRawTransaction(
|
||||
transaction.serialize()
|
||||
);
|
||||
await connection.confirmTransaction(txid);
|
||||
} catch (e) {
|
||||
// this could fail because the token is already attested (in an unclean env)
|
||||
}
|
||||
provider.destroy();
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done(
|
||||
"An error occurred while trying to attest from Ethereum to Solana"
|
||||
);
|
||||
}
|
||||
})();
|
||||
});
|
||||
// TODO: it is attested
|
||||
test("Send Ethereum ERC-20 to Solana", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
// create a keypair for Solana
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
|
||||
// determine destination address - an associated token account
|
||||
const solanaMintKey = new PublicKey(
|
||||
(await getForeignAssetSolana(
|
||||
connection,
|
||||
SOLANA_TOKEN_BRIDGE_ADDRESS,
|
||||
CHAIN_ID_ETH,
|
||||
hexToUint8Array(nativeToHexString(TEST_ERC20, CHAIN_ID_ETH) || "")
|
||||
)) || ""
|
||||
);
|
||||
const recipient = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
solanaMintKey,
|
||||
keypair.publicKey
|
||||
);
|
||||
// create the associated token account if it doesn't exist
|
||||
const associatedAddressInfo = await connection.getAccountInfo(
|
||||
recipient
|
||||
);
|
||||
if (!associatedAddressInfo) {
|
||||
const transaction = new Transaction().add(
|
||||
await Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
solanaMintKey,
|
||||
recipient,
|
||||
keypair.publicKey, // owner
|
||||
keypair.publicKey // payer
|
||||
)
|
||||
);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = keypair.publicKey;
|
||||
// sign, send, and confirm transaction
|
||||
transaction.partialSign(keypair);
|
||||
const txid = await connection.sendRawTransaction(
|
||||
transaction.serialize()
|
||||
);
|
||||
await connection.confirmTransaction(txid);
|
||||
}
|
||||
// create a signer for Eth
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
|
||||
const amount = parseUnits("1", 18);
|
||||
const fee = parseUnits("1", 12);
|
||||
const transferAmount = amount.add(fee);
|
||||
// approve the bridge to spend tokens
|
||||
await approveEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
TEST_ERC20,
|
||||
signer,
|
||||
transferAmount
|
||||
);
|
||||
// transfer tokens
|
||||
const receipt = await transferFromEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
signer,
|
||||
TEST_ERC20,
|
||||
transferAmount,
|
||||
CHAIN_ID_SOLANA,
|
||||
hexToUint8Array(
|
||||
nativeToHexString(recipient.toString(), CHAIN_ID_SOLANA) || ""
|
||||
),
|
||||
fee
|
||||
);
|
||||
// get the sequence from the logs (needed to fetch the vaa)
|
||||
sequence = parseSequenceFromLogEth(receipt, ETH_CORE_BRIDGE_ADDRESS);
|
||||
emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
||||
// poll until the guardian(s) witness and sign the vaa
|
||||
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
}
|
||||
);
|
||||
console.log("Got signed vaa: ", signedVAA);
|
||||
transferSignedVAA = signedVAA;
|
||||
provider.destroy();
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done("An error occurred while trying to send from Ethereum to Solana");
|
||||
}
|
||||
})();
|
||||
});
|
||||
test("Spy Relay redeemed on Sol", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
let success: boolean = false;
|
||||
for (let count = 0; count < 5 && !success; ++count) {
|
||||
console.log(
|
||||
"sleeping before querying spy relay",
|
||||
new Date().toLocaleString()
|
||||
);
|
||||
await sleep(5000);
|
||||
success = await getIsTransferCompletedSolana(
|
||||
SOLANA_TOKEN_BRIDGE_ADDRESS,
|
||||
transferSignedVAA,
|
||||
connection
|
||||
);
|
||||
console.log(
|
||||
"getIsTransferCompletedSolana returned %d, count is %d",
|
||||
success,
|
||||
count
|
||||
);
|
||||
}
|
||||
expect(success).toBe(true);
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done("An error occurred while trying to redeem on Sol");
|
||||
}
|
||||
})();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ethereum to Terra Classic", () => {
|
||||
test("Attest Ethereum ERC-20 to Terra Classic", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
// create a signer for Eth
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
|
||||
// attest the test token
|
||||
const receipt = await attestFromEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
signer,
|
||||
TEST_ERC20
|
||||
);
|
||||
// get the sequence from the logs (needed to fetch the vaa)
|
||||
const sequence = parseSequenceFromLogEth(
|
||||
receipt,
|
||||
ETH_CORE_BRIDGE_ADDRESS
|
||||
);
|
||||
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
||||
// poll until the guardian(s) witness and sign the vaa
|
||||
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
}
|
||||
);
|
||||
const lcd = new LCDClient({
|
||||
URL: TERRA_NODE_URL,
|
||||
chainID: TERRA_CHAIN_ID,
|
||||
isClassic: true,
|
||||
});
|
||||
const mk = new MnemonicKey({
|
||||
mnemonic: TERRA_PRIVATE_KEY,
|
||||
});
|
||||
const wallet = lcd.wallet(mk);
|
||||
const msg = await createWrappedOnTerra(
|
||||
TERRA_TOKEN_BRIDGE_ADDRESS,
|
||||
wallet.key.accAddress,
|
||||
signedVAA
|
||||
);
|
||||
const gasPrices = await axios
|
||||
.get(TERRA_GAS_PRICES_URL)
|
||||
.then((result) => result.data);
|
||||
const account = await lcd.auth.accountInfo(wallet.key.accAddress);
|
||||
const feeEstimate = await lcd.tx.estimateFee(
|
||||
[
|
||||
{
|
||||
sequenceNumber: account.getSequenceNumber(),
|
||||
publicKey: account.getPublicKey(),
|
||||
},
|
||||
],
|
||||
{
|
||||
msgs: [msg],
|
||||
feeDenoms: ["uluna"],
|
||||
gasPrices,
|
||||
}
|
||||
);
|
||||
const tx = await wallet.createAndSignTx({
|
||||
msgs: [msg],
|
||||
memo: "test",
|
||||
feeDenoms: ["uluna"],
|
||||
gasPrices,
|
||||
fee: feeEstimate,
|
||||
});
|
||||
await lcd.tx.broadcast(tx);
|
||||
provider.destroy();
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done(
|
||||
"An error occurred while trying to attest from Ethereum to Terra Classic"
|
||||
);
|
||||
}
|
||||
})();
|
||||
});
|
||||
// TODO: it is attested
|
||||
test("Send Ethereum ERC-20 to Terra Classic", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
// create a signer for Eth
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
|
||||
const amount = parseUnits("1", 18);
|
||||
const fee = parseUnits("1", 12);
|
||||
const transferAmount = amount.add(fee);
|
||||
// approve the bridge to spend tokens
|
||||
await approveEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
TEST_ERC20,
|
||||
signer,
|
||||
transferAmount
|
||||
);
|
||||
const lcd = new LCDClient({
|
||||
URL: TERRA_NODE_URL,
|
||||
chainID: TERRA_CHAIN_ID,
|
||||
isClassic: true,
|
||||
});
|
||||
const mk = new MnemonicKey({
|
||||
mnemonic: TERRA_PRIVATE_KEY,
|
||||
});
|
||||
const wallet = lcd.wallet(mk);
|
||||
// transfer tokens
|
||||
const receipt = await transferFromEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
signer,
|
||||
TEST_ERC20,
|
||||
transferAmount,
|
||||
CHAIN_ID_TERRA,
|
||||
hexToUint8Array(
|
||||
nativeToHexString(wallet.key.accAddress, CHAIN_ID_TERRA) || ""
|
||||
),
|
||||
fee
|
||||
);
|
||||
// get the sequence from the logs (needed to fetch the vaa)
|
||||
sequence = parseSequenceFromLogEth(receipt, ETH_CORE_BRIDGE_ADDRESS);
|
||||
emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
||||
// poll until the guardian(s) witness and sign the vaa
|
||||
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
}
|
||||
);
|
||||
console.log("Got signed vaa: ", signedVAA);
|
||||
transferSignedVAA = signedVAA;
|
||||
provider.destroy();
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done(
|
||||
"An error occurred while trying to send from Ethereum to Terra Classic"
|
||||
);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
test("Spy Relay redeemed on Terra Classic", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
const lcd = new LCDClient({
|
||||
URL: TERRA_NODE_URL,
|
||||
chainID: TERRA_CHAIN_ID,
|
||||
isClassic: true,
|
||||
});
|
||||
var success: boolean = false;
|
||||
for (let count = 0; count < 5 && !success; ++count) {
|
||||
console.log(
|
||||
"sleeping before querying spy relay",
|
||||
new Date().toLocaleString()
|
||||
);
|
||||
await sleep(5000);
|
||||
success = await await getIsTransferCompletedTerra(
|
||||
TERRA_TOKEN_BRIDGE_ADDRESS,
|
||||
transferSignedVAA,
|
||||
lcd,
|
||||
TERRA_GAS_PRICES_URL
|
||||
);
|
||||
console.log(
|
||||
"getIsTransferCompletedTerra returned %d, count is %d",
|
||||
success,
|
||||
count
|
||||
);
|
||||
}
|
||||
expect(success).toBe(true);
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done(
|
||||
"An error occurred while checking to see if redeem on Terra Classic was successful"
|
||||
);
|
||||
}
|
||||
})();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ethereum to Terra", () => {
|
||||
test("Attest Ethereum ERC-20 to Terra", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
// create a signer for Eth
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
|
||||
// attest the test token
|
||||
const receipt = await attestFromEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
signer,
|
||||
TEST_ERC20
|
||||
);
|
||||
// get the sequence from the logs (needed to fetch the vaa)
|
||||
const sequence = parseSequenceFromLogEth(
|
||||
receipt,
|
||||
ETH_CORE_BRIDGE_ADDRESS
|
||||
);
|
||||
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
||||
// poll until the guardian(s) witness and sign the vaa
|
||||
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
}
|
||||
);
|
||||
const lcd = new LCDClient({
|
||||
URL: TERRA2_NODE_URL,
|
||||
chainID: TERRA_CHAIN_ID,
|
||||
isClassic: false,
|
||||
});
|
||||
const mk = new MnemonicKey({
|
||||
mnemonic: TERRA_PRIVATE_KEY,
|
||||
});
|
||||
const wallet = lcd.wallet(mk);
|
||||
const msg = await createWrappedOnTerra(
|
||||
TERRA2_TOKEN_BRIDGE_ADDRESS,
|
||||
wallet.key.accAddress,
|
||||
signedVAA
|
||||
);
|
||||
const gasPrices = await axios
|
||||
.get(TERRA2_GAS_PRICES_URL)
|
||||
.then((result) => result.data);
|
||||
const account = await lcd.auth.accountInfo(wallet.key.accAddress);
|
||||
const feeEstimate = await lcd.tx.estimateFee(
|
||||
[
|
||||
{
|
||||
sequenceNumber: account.getSequenceNumber(),
|
||||
publicKey: account.getPublicKey(),
|
||||
},
|
||||
],
|
||||
{
|
||||
msgs: [msg],
|
||||
feeDenoms: ["uluna"],
|
||||
gasPrices,
|
||||
}
|
||||
);
|
||||
const tx = await wallet.createAndSignTx({
|
||||
msgs: [msg],
|
||||
memo: "test",
|
||||
feeDenoms: ["uluna"],
|
||||
gasPrices,
|
||||
fee: feeEstimate,
|
||||
});
|
||||
await lcd.tx.broadcast(tx);
|
||||
provider.destroy();
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done("An error occurred while trying to attest from Ethereum to Terra");
|
||||
}
|
||||
})();
|
||||
});
|
||||
// TODO: it is attested
|
||||
test("Send Ethereum ERC-20 to Terra", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
// create a signer for Eth
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
|
||||
const amount = parseUnits("1", 18);
|
||||
const fee = parseUnits("1", 12);
|
||||
const transferAmount = amount.add(fee);
|
||||
// approve the bridge to spend tokens
|
||||
await approveEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
TEST_ERC20,
|
||||
signer,
|
||||
transferAmount
|
||||
);
|
||||
const lcd = new LCDClient({
|
||||
URL: TERRA2_NODE_URL,
|
||||
chainID: TERRA_CHAIN_ID,
|
||||
isClassic: false,
|
||||
});
|
||||
const mk = new MnemonicKey({
|
||||
mnemonic: TERRA_PRIVATE_KEY,
|
||||
});
|
||||
const wallet = lcd.wallet(mk);
|
||||
// transfer tokens
|
||||
const receipt = await transferFromEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
signer,
|
||||
TEST_ERC20,
|
||||
transferAmount,
|
||||
CHAIN_ID_TERRA2,
|
||||
hexToUint8Array(
|
||||
nativeToHexString(wallet.key.accAddress, CHAIN_ID_TERRA2) || ""
|
||||
),
|
||||
fee
|
||||
);
|
||||
// get the sequence from the logs (needed to fetch the vaa)
|
||||
sequence = parseSequenceFromLogEth(receipt, ETH_CORE_BRIDGE_ADDRESS);
|
||||
emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
||||
// poll until the guardian(s) witness and sign the vaa
|
||||
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
}
|
||||
);
|
||||
console.log("Got signed vaa: ", signedVAA);
|
||||
transferSignedVAA = signedVAA;
|
||||
provider.destroy();
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done("An error occurred while trying to send from Ethereum to Terra");
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
test("Spy Relay redeemed on Terra", (done) => {
|
||||
(async () => {
|
||||
try {
|
||||
const lcd = new LCDClient({
|
||||
URL: TERRA2_NODE_URL,
|
||||
chainID: TERRA_CHAIN_ID,
|
||||
isClassic: false,
|
||||
});
|
||||
var success: boolean = false;
|
||||
for (let count = 0; count < 5 && !success; ++count) {
|
||||
console.log(
|
||||
"sleeping before querying spy relay",
|
||||
new Date().toLocaleString()
|
||||
);
|
||||
await sleep(5000);
|
||||
success = await await getIsTransferCompletedTerra(
|
||||
TERRA2_TOKEN_BRIDGE_ADDRESS,
|
||||
transferSignedVAA,
|
||||
lcd,
|
||||
TERRA2_GAS_PRICES_URL
|
||||
);
|
||||
console.log(
|
||||
"getIsTransferCompletedTerra returned %d, count is %d",
|
||||
success,
|
||||
count
|
||||
);
|
||||
}
|
||||
expect(success).toBe(true);
|
||||
done();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done(
|
||||
"An error occurred while checking to see if redeem on Terra was successful"
|
||||
);
|
||||
}
|
||||
})();
|
||||
});
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
import { Backend, Relayer, Listener } from "../definitions";
|
||||
import { TokenBridgeListener } from "./listener";
|
||||
import { TokenBridgeRelayer } from "./relayer";
|
||||
|
||||
/** Payload version 1 token bridge listener and relayer backend */
|
||||
const backend: Backend = {
|
||||
relayer: new TokenBridgeRelayer(),
|
||||
listener: new TokenBridgeListener(),
|
||||
};
|
||||
|
||||
export default backend;
|
|
@ -1,278 +0,0 @@
|
|||
/** The default backend is relaying payload 1 token bridge messages only */
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
uint8ArrayToHex,
|
||||
tryHexToNativeString,
|
||||
getEmitterAddressEth,
|
||||
getEmitterAddressSolana,
|
||||
getEmitterAddressTerra,
|
||||
parseTransferPayload,
|
||||
isTerraChain,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { getListenerEnvironment } from "../../configureEnv";
|
||||
import { getScopedLogger, ScopedLogger } from "../../helpers/logHelper";
|
||||
import {
|
||||
ParsedVaa,
|
||||
ParsedTransferPayload,
|
||||
parseVaaTyped,
|
||||
} from "../../listener/validation";
|
||||
import { TypedFilter, Listener } from "../definitions";
|
||||
import {
|
||||
initPayloadWithVAA,
|
||||
storeInRedis,
|
||||
checkQueue,
|
||||
StoreKey,
|
||||
storeKeyFromParsedVAA,
|
||||
storeKeyToJson,
|
||||
StorePayload,
|
||||
storePayloadToJson,
|
||||
} from "../../helpers/redisHelper";
|
||||
|
||||
async function encodeEmitterAddress(
|
||||
myChainId: ChainId,
|
||||
emitterAddressStr: string
|
||||
): Promise<string> {
|
||||
if (myChainId === CHAIN_ID_SOLANA) {
|
||||
return await getEmitterAddressSolana(emitterAddressStr);
|
||||
}
|
||||
|
||||
if (isTerraChain(myChainId)) {
|
||||
return await getEmitterAddressTerra(emitterAddressStr);
|
||||
}
|
||||
|
||||
return getEmitterAddressEth(emitterAddressStr);
|
||||
}
|
||||
|
||||
/** Listener for payload 1 token bridge messages only */
|
||||
export class TokenBridgeListener implements Listener {
|
||||
logger: ScopedLogger;
|
||||
|
||||
/**
|
||||
* @throws - when the listener environment setup fails
|
||||
*/
|
||||
constructor() {
|
||||
this.logger = getScopedLogger(["TokenBridgeListener"]);
|
||||
}
|
||||
|
||||
/** Verify this payload is version 1. */
|
||||
verifyIsPayloadV1(parsedVaa: ParsedVaa<Uint8Array>): boolean {
|
||||
if (parsedVaa.payload[0] !== 1) {
|
||||
this.logger.debug("Specified vaa is not payload version 1.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Verify this payload has a fee specified for relaying. */
|
||||
verifyFeeSpecified(payload: ParsedTransferPayload): boolean {
|
||||
/**
|
||||
* TODO: simulate gas fees / get notional from coingecko and ensure the fees cover the relay.
|
||||
* We might just keep this check here but verify the notional is enough to pay the gas
|
||||
* fees in the actual relayer. That way we can retry up to the max number of retries
|
||||
* and if the gas fluctuates we might be able to make it still.
|
||||
*/
|
||||
|
||||
/** Is the specified fee sufficient to relay? */
|
||||
const sufficientFee = payload.fee && payload.fee > BigInt(0);
|
||||
|
||||
if (!sufficientFee) {
|
||||
this.logger.debug("Token transfer does not have a sufficient fee.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Verify the the token in this payload in the approved token list. */
|
||||
verifyIsApprovedToken(payload: ParsedTransferPayload): boolean {
|
||||
let originAddressNative: string;
|
||||
let env = getListenerEnvironment();
|
||||
try {
|
||||
originAddressNative = tryHexToNativeString(
|
||||
payload.originAddress,
|
||||
payload.originChain
|
||||
);
|
||||
} catch (e: any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Token is in the SUPPORTED_TOKENS env var config
|
||||
const isApprovedToken = env.supportedTokens.find((token) => {
|
||||
return (
|
||||
originAddressNative &&
|
||||
token.address.toLowerCase() === originAddressNative.toLowerCase() &&
|
||||
token.chainId === payload.originChain
|
||||
);
|
||||
});
|
||||
|
||||
if (!isApprovedToken) {
|
||||
this.logger.debug("Token transfer is not for an approved token.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Parses a raw VAA byte array
|
||||
*
|
||||
* @throws when unable to parse the VAA
|
||||
*/
|
||||
public async parseVaa(rawVaa: Uint8Array): Promise<ParsedVaa<Uint8Array>> {
|
||||
let parsedVaa: ParsedVaa<Uint8Array> | null = null;
|
||||
|
||||
try {
|
||||
parsedVaa = await parseVaaTyped(rawVaa);
|
||||
} catch (e) {
|
||||
this.logger.error("Encountered error while parsing raw VAA " + e);
|
||||
}
|
||||
if (!parsedVaa) {
|
||||
throw new Error("Unable to parse the specified VAA.");
|
||||
}
|
||||
|
||||
return parsedVaa;
|
||||
}
|
||||
|
||||
/** Parse the VAA and return the payload nicely typed */
|
||||
public async parsePayload(
|
||||
rawPayload: Uint8Array
|
||||
): Promise<ParsedTransferPayload> {
|
||||
let parsedPayload: any;
|
||||
try {
|
||||
parsedPayload = parseTransferPayload(Buffer.from(rawPayload));
|
||||
} catch (e) {
|
||||
this.logger.error("Encountered error while parsing vaa payload" + e);
|
||||
}
|
||||
|
||||
if (!parsedPayload) {
|
||||
this.logger.debug("Failed to parse the transfer payload.");
|
||||
throw new Error("Could not parse the transfer payload.");
|
||||
}
|
||||
return parsedPayload;
|
||||
}
|
||||
|
||||
/** Verify this is a VAA we want to relay. */
|
||||
public async validate(
|
||||
rawVaa: Uint8Array
|
||||
): Promise<ParsedVaa<ParsedTransferPayload> | string> {
|
||||
let parsedVaa = await this.parseVaa(rawVaa);
|
||||
let parsedPayload: ParsedTransferPayload;
|
||||
|
||||
// Verify this is actually a token bridge transfer payload
|
||||
if (!this.verifyIsPayloadV1(parsedVaa)) {
|
||||
return "Wrong payload type";
|
||||
}
|
||||
try {
|
||||
parsedPayload = await this.parsePayload(parsedVaa.payload);
|
||||
} catch (e: any) {
|
||||
return "Payload parsing failure";
|
||||
}
|
||||
|
||||
// Verify we want to relay this request
|
||||
if (
|
||||
!this.verifyIsApprovedToken(parsedPayload) ||
|
||||
!this.verifyFeeSpecified(parsedPayload)
|
||||
) {
|
||||
return "Validation failed";
|
||||
}
|
||||
|
||||
// Great success!
|
||||
return { ...parsedVaa, payload: parsedPayload };
|
||||
}
|
||||
|
||||
/** Get spy filters for all emitters we care about */
|
||||
public async getEmitterFilters(): Promise<TypedFilter[]> {
|
||||
let env = getListenerEnvironment();
|
||||
let filters: {
|
||||
emitterFilter: { chainId: ChainId; emitterAddress: string };
|
||||
}[] = [];
|
||||
for (let i = 0; i < env.spyServiceFilters.length; i++) {
|
||||
const filter = env.spyServiceFilters[i];
|
||||
this.logger.info(
|
||||
"Getting spyServiceFilter[" +
|
||||
i +
|
||||
"]: chainId = " +
|
||||
filter.chainId +
|
||||
", emmitterAddress = [" +
|
||||
filter.emitterAddress +
|
||||
"]"
|
||||
);
|
||||
const typedFilter = {
|
||||
emitterFilter: {
|
||||
chainId: filter.chainId as ChainId,
|
||||
emitterAddress: await encodeEmitterAddress(
|
||||
filter.chainId,
|
||||
filter.emitterAddress
|
||||
),
|
||||
},
|
||||
};
|
||||
this.logger.info(
|
||||
"adding filter: chainId: [" +
|
||||
typedFilter.emitterFilter.chainId +
|
||||
"], emitterAddress: [" +
|
||||
typedFilter.emitterFilter.emitterAddress +
|
||||
"]"
|
||||
);
|
||||
filters.push(typedFilter);
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
/** Process and validate incoming VAAs from the spy. */
|
||||
public async process(rawVaa: Uint8Array): Promise<void> {
|
||||
// TODO: Use a type guard function to verify the ParsedVaa type too?
|
||||
const validationResults: ParsedVaa<ParsedTransferPayload> | string =
|
||||
await this.validate(rawVaa);
|
||||
|
||||
if (typeof validationResults === "string") {
|
||||
this.logger.debug(`Skipping spied request: ${validationResults}`);
|
||||
return;
|
||||
}
|
||||
const parsedVaa: ParsedVaa<ParsedTransferPayload> = validationResults;
|
||||
|
||||
const redisKey: StoreKey = storeKeyFromParsedVAA(parsedVaa);
|
||||
const isQueued = await checkQueue(storeKeyToJson(redisKey));
|
||||
if (isQueued) {
|
||||
this.logger.error(`Not storing in redis: ${isQueued}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
"forwarding vaa to relayer: emitter: [" +
|
||||
parsedVaa.emitterChain +
|
||||
":" +
|
||||
uint8ArrayToHex(parsedVaa.emitterAddress) +
|
||||
"], seqNum: " +
|
||||
parsedVaa.sequence +
|
||||
", payload: origin: [" +
|
||||
parsedVaa.payload.originAddress +
|
||||
":" +
|
||||
parsedVaa.payload.originAddress +
|
||||
"], target: [" +
|
||||
parsedVaa.payload.targetChain +
|
||||
":" +
|
||||
parsedVaa.payload.targetAddress +
|
||||
"], amount: " +
|
||||
parsedVaa.payload.amount +
|
||||
"], fee: " +
|
||||
parsedVaa.payload.fee +
|
||||
", "
|
||||
);
|
||||
|
||||
const redisPayload: StorePayload = initPayloadWithVAA(
|
||||
uint8ArrayToHex(rawVaa)
|
||||
);
|
||||
|
||||
await this.store(redisKey, redisPayload);
|
||||
}
|
||||
|
||||
public async store(key: StoreKey, payload: StorePayload): Promise<void> {
|
||||
let serializedKey = storeKeyToJson(key);
|
||||
let serializedPayload = storePayloadToJson(payload);
|
||||
|
||||
this.logger.debug(
|
||||
`storing: key: [${key.chain_id}/${key.emitter_address}/${key.sequence}], payload: [${serializedPayload}]`
|
||||
);
|
||||
|
||||
return await storeInRedis(serializedKey, serializedPayload);
|
||||
}
|
||||
}
|
|
@ -1,394 +0,0 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
tryHexToNativeString,
|
||||
hexToUint8Array,
|
||||
importCoreWasm,
|
||||
isEVMChain,
|
||||
parseTransferPayload,
|
||||
CHAIN_ID_UNSET,
|
||||
isTerraChain,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
|
||||
import { REDIS_RETRY_MS, AUDIT_INTERVAL_MS, Relayer } from "../definitions";
|
||||
import { getScopedLogger, ScopedLogger } from "../../helpers/logHelper";
|
||||
import {
|
||||
connectToRedis,
|
||||
RedisTables,
|
||||
RelayResult,
|
||||
resetPayload,
|
||||
Status,
|
||||
StorePayload,
|
||||
storePayloadFromJson,
|
||||
storePayloadToJson,
|
||||
WorkerInfo,
|
||||
} from "../../helpers/redisHelper";
|
||||
import { PromHelper } from "../../helpers/promHelpers";
|
||||
import { sleep } from "../../helpers/utils";
|
||||
import { relayTerra } from "../../relayer/terra";
|
||||
import { relaySolana } from "../../relayer/solana";
|
||||
import { relayEVM } from "../../relayer/evm";
|
||||
import { getRelayerEnvironment } from "../../configureEnv";
|
||||
|
||||
function getChainConfigInfo(chainId: ChainId) {
|
||||
const env = getRelayerEnvironment();
|
||||
return env.supportedChains.find((x) => x.chainId === chainId);
|
||||
}
|
||||
|
||||
/** Relayer for payload 1 token bridge messages only */
|
||||
export class TokenBridgeRelayer implements Relayer {
|
||||
/** Process the relay request */
|
||||
async process(
|
||||
key: string,
|
||||
privateKey: any,
|
||||
relayLogger: ScopedLogger,
|
||||
metrics: PromHelper
|
||||
): Promise<void> {
|
||||
const logger = getScopedLogger(["TokenBridgeRelayer.process"], relayLogger);
|
||||
try {
|
||||
logger.debug("Processing request %s...", key);
|
||||
// Get the entry from the working store
|
||||
const redisClient = await connectToRedis();
|
||||
if (!redisClient) {
|
||||
logger.error("Failed to connect to Redis in processRequest");
|
||||
return;
|
||||
}
|
||||
await redisClient.select(RedisTables.WORKING);
|
||||
let value: string | null = await redisClient.get(key);
|
||||
if (!value) {
|
||||
logger.error("Could not find key %s", key);
|
||||
return;
|
||||
}
|
||||
let payload: StorePayload = storePayloadFromJson(value);
|
||||
if (payload.status !== Status.Pending) {
|
||||
logger.info("This key %s has already been processed.", key);
|
||||
return;
|
||||
}
|
||||
// Actually do the processing here and update status and time field
|
||||
let relayResult: RelayResult;
|
||||
try {
|
||||
if (payload.retries > 0) {
|
||||
logger.info(
|
||||
"Calling with vaa_bytes %s, retry %d",
|
||||
payload.vaa_bytes,
|
||||
payload.retries
|
||||
);
|
||||
} else {
|
||||
logger.info("Calling with vaa_bytes %s", payload.vaa_bytes);
|
||||
}
|
||||
relayResult = await this.relay(
|
||||
payload.vaa_bytes,
|
||||
false,
|
||||
privateKey,
|
||||
logger,
|
||||
metrics
|
||||
);
|
||||
logger.info("Relay returned: %o", Status[relayResult.status]);
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
logger.error("Failed to relay transfer vaa: %s", e.message);
|
||||
} else {
|
||||
logger.error("Failed to relay transfer vaa: %o", e);
|
||||
}
|
||||
|
||||
relayResult = {
|
||||
status: Status.Error,
|
||||
result: e && e?.message !== undefined ? e.message : "Failure",
|
||||
};
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 10;
|
||||
let targetChain: ChainId = CHAIN_ID_UNSET;
|
||||
try {
|
||||
const { parse_vaa } = await importCoreWasm();
|
||||
const parsedVAA = parse_vaa(hexToUint8Array(payload.vaa_bytes));
|
||||
const transferPayload = parseTransferPayload(
|
||||
Buffer.from(parsedVAA.payload)
|
||||
);
|
||||
targetChain = transferPayload.targetChain;
|
||||
} catch (e) {}
|
||||
let retry: boolean = false;
|
||||
if (relayResult.status !== Status.Completed) {
|
||||
metrics.incFailures(targetChain);
|
||||
if (payload.retries >= MAX_RETRIES) {
|
||||
relayResult.status = Status.FatalError;
|
||||
}
|
||||
if (relayResult.status === Status.FatalError) {
|
||||
// Invoke fatal error logic here!
|
||||
payload.retries = MAX_RETRIES;
|
||||
} else {
|
||||
// Invoke retry logic here!
|
||||
retry = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Put result back into store
|
||||
payload.status = relayResult.status;
|
||||
payload.timestamp = new Date().toISOString();
|
||||
payload.retries++;
|
||||
value = storePayloadToJson(payload);
|
||||
if (!retry || payload.retries > MAX_RETRIES) {
|
||||
await redisClient.set(key, value);
|
||||
} else {
|
||||
// Remove from the working table
|
||||
await redisClient.del(key);
|
||||
// Put this back into the incoming table
|
||||
await redisClient.select(RedisTables.INCOMING);
|
||||
await redisClient.set(key, value);
|
||||
}
|
||||
await redisClient.quit();
|
||||
} catch (e: any) {
|
||||
logger.error("Unexpected error in processRequest: " + e.message);
|
||||
logger.error("request key: " + key);
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Run one audit thread per worker so that auditors can not block other auditors or workers */
|
||||
async runAuditor(workerInfo: WorkerInfo, metrics: PromHelper): Promise<void> {
|
||||
const auditLogger = getScopedLogger([
|
||||
`audit-worker-${workerInfo.targetChainName}-${workerInfo.index}`,
|
||||
]);
|
||||
while (true) {
|
||||
try {
|
||||
let redisClient: any = null;
|
||||
while (!redisClient) {
|
||||
redisClient = await connectToRedis();
|
||||
if (!redisClient) {
|
||||
auditLogger.error("Failed to connect to redis!");
|
||||
await sleep(REDIS_RETRY_MS);
|
||||
}
|
||||
}
|
||||
await redisClient.select(RedisTables.WORKING);
|
||||
for await (const si_key of redisClient.scanIterator()) {
|
||||
const si_value = await redisClient.get(si_key);
|
||||
if (!si_value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const storePayload: StorePayload = storePayloadFromJson(si_value);
|
||||
try {
|
||||
const { parse_vaa } = await importCoreWasm();
|
||||
const parsedVAA = parse_vaa(
|
||||
hexToUint8Array(storePayload.vaa_bytes)
|
||||
);
|
||||
const payloadBuffer: Buffer = Buffer.from(parsedVAA.payload);
|
||||
const transferPayload = parseTransferPayload(payloadBuffer);
|
||||
|
||||
const chain = transferPayload.targetChain;
|
||||
if (chain !== workerInfo.targetChainId) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
auditLogger.error("Failed to parse a stored VAA: " + e);
|
||||
auditLogger.error("si_value of failure: " + si_value);
|
||||
continue;
|
||||
}
|
||||
auditLogger.debug(
|
||||
"key %s => status: %s, timestamp: %s, retries: %d",
|
||||
si_key,
|
||||
Status[storePayload.status],
|
||||
storePayload.timestamp,
|
||||
storePayload.retries
|
||||
);
|
||||
// Let things sit in here for 10 minutes
|
||||
// After that:
|
||||
// - Toss totally failed VAAs
|
||||
// - Check to see if successful transactions were rolled back
|
||||
// - Put roll backs into INCOMING table
|
||||
// - Toss legitimately completed transactions
|
||||
const now = new Date();
|
||||
const old = new Date(storePayload.timestamp);
|
||||
const timeDelta = now.getTime() - old.getTime(); // delta is in mS
|
||||
const TEN_MINUTES = 600000;
|
||||
auditLogger.debug(
|
||||
"Checking timestamps: now: " +
|
||||
now.toISOString() +
|
||||
", old: " +
|
||||
old.toISOString() +
|
||||
", delta: " +
|
||||
timeDelta
|
||||
);
|
||||
if (timeDelta > TEN_MINUTES) {
|
||||
// Deal with this item
|
||||
if (storePayload.status === Status.FatalError) {
|
||||
// Done with this failed transaction
|
||||
auditLogger.debug("Discarding FatalError.");
|
||||
await redisClient.del(si_key);
|
||||
continue;
|
||||
} else if (storePayload.status === Status.Completed) {
|
||||
// Check for rollback
|
||||
auditLogger.debug("Checking for rollback.");
|
||||
|
||||
//TODO actually do an isTransferCompleted
|
||||
const rr = await this.relay(
|
||||
storePayload.vaa_bytes,
|
||||
true,
|
||||
workerInfo.walletPrivateKey,
|
||||
auditLogger,
|
||||
metrics
|
||||
);
|
||||
|
||||
await redisClient.del(si_key);
|
||||
if (rr.status === Status.Completed) {
|
||||
metrics.incConfirmed(workerInfo.targetChainId);
|
||||
} else {
|
||||
auditLogger.info("Detected a rollback on " + si_key);
|
||||
metrics.incRollback(workerInfo.targetChainId);
|
||||
// Remove this item from the WORKING table and move it to INCOMING
|
||||
await redisClient.select(RedisTables.INCOMING);
|
||||
await redisClient.set(
|
||||
si_key,
|
||||
storePayloadToJson(
|
||||
resetPayload(storePayloadFromJson(si_value))
|
||||
)
|
||||
);
|
||||
await redisClient.select(RedisTables.WORKING);
|
||||
}
|
||||
} else if (storePayload.status === Status.Error) {
|
||||
auditLogger.error("Received Error status.");
|
||||
continue;
|
||||
} else if (storePayload.status === Status.Pending) {
|
||||
auditLogger.error("Received Pending status.");
|
||||
continue;
|
||||
} else {
|
||||
auditLogger.error("Unhandled Status of " + storePayload.status);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
redisClient.quit();
|
||||
// metrics.setDemoWalletBalance(now.getUTCSeconds());
|
||||
} catch (e) {
|
||||
auditLogger.error("spawnAuditorThread: caught exception: " + e);
|
||||
}
|
||||
await sleep(AUDIT_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
/** Parse the target chain id from the payload */
|
||||
targetChainId(payload: Buffer): ChainId {
|
||||
const transferPayload = parseTransferPayload(payload);
|
||||
return transferPayload.targetChain;
|
||||
}
|
||||
|
||||
async relay(
|
||||
signedVAA: string,
|
||||
checkOnly: boolean,
|
||||
walletPrivateKey: any,
|
||||
relayLogger: ScopedLogger,
|
||||
metrics: PromHelper
|
||||
): Promise<RelayResult> {
|
||||
const logger = getScopedLogger(["relay"], relayLogger);
|
||||
const { parse_vaa } = await importCoreWasm();
|
||||
const parsedVAA = parse_vaa(hexToUint8Array(signedVAA));
|
||||
if (parsedVAA.payload[0] === 1) {
|
||||
const transferPayload = parseTransferPayload(
|
||||
Buffer.from(parsedVAA.payload)
|
||||
);
|
||||
|
||||
const chainConfigInfo = getChainConfigInfo(transferPayload.targetChain);
|
||||
if (!chainConfigInfo) {
|
||||
logger.error(
|
||||
"relay: improper chain ID: " + transferPayload.targetChain
|
||||
);
|
||||
return {
|
||||
status: Status.FatalError,
|
||||
result:
|
||||
"Fatal Error: target chain " +
|
||||
transferPayload.targetChain +
|
||||
" not supported",
|
||||
};
|
||||
}
|
||||
|
||||
if (isEVMChain(transferPayload.targetChain)) {
|
||||
let nativeOrigin: string;
|
||||
try {
|
||||
nativeOrigin = tryHexToNativeString(
|
||||
transferPayload.originAddress,
|
||||
transferPayload.originChain
|
||||
);
|
||||
} catch (e: any) {
|
||||
return {
|
||||
status: Status.Error,
|
||||
result: `error converting origin address: ${e?.message}`,
|
||||
};
|
||||
}
|
||||
const unwrapNative =
|
||||
transferPayload.originChain === transferPayload.targetChain &&
|
||||
nativeOrigin?.toLowerCase() ===
|
||||
chainConfigInfo.wrappedAsset?.toLowerCase();
|
||||
logger.debug(
|
||||
"isEVMChain: originAddress: [" +
|
||||
transferPayload.originAddress +
|
||||
"], wrappedAsset: [" +
|
||||
chainConfigInfo.wrappedAsset +
|
||||
"], unwrapNative: " +
|
||||
unwrapNative
|
||||
);
|
||||
let evmResult = await relayEVM(
|
||||
chainConfigInfo,
|
||||
signedVAA,
|
||||
unwrapNative,
|
||||
checkOnly,
|
||||
walletPrivateKey,
|
||||
logger,
|
||||
metrics
|
||||
);
|
||||
return {
|
||||
status: evmResult.redeemed ? Status.Completed : Status.Error,
|
||||
result: evmResult.result.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
if (transferPayload.targetChain === CHAIN_ID_SOLANA) {
|
||||
let rResult: RelayResult = { status: Status.Error, result: "" };
|
||||
const retVal = await relaySolana(
|
||||
chainConfigInfo,
|
||||
signedVAA,
|
||||
checkOnly,
|
||||
walletPrivateKey,
|
||||
logger,
|
||||
metrics
|
||||
);
|
||||
if (retVal.redeemed) {
|
||||
rResult.status = Status.Completed;
|
||||
}
|
||||
rResult.result = retVal.result;
|
||||
return rResult;
|
||||
}
|
||||
|
||||
if (isTerraChain(transferPayload.targetChain)) {
|
||||
let rResult: RelayResult = { status: Status.Error, result: "" };
|
||||
const retVal = await relayTerra(
|
||||
chainConfigInfo,
|
||||
signedVAA,
|
||||
checkOnly,
|
||||
walletPrivateKey,
|
||||
logger,
|
||||
metrics
|
||||
);
|
||||
if (retVal.redeemed) {
|
||||
rResult.status = Status.Completed;
|
||||
}
|
||||
rResult.result = retVal.result;
|
||||
return rResult;
|
||||
}
|
||||
|
||||
logger.error(
|
||||
"relay: target chain ID: " +
|
||||
transferPayload.targetChain +
|
||||
" is invalid, this is a program bug!"
|
||||
);
|
||||
|
||||
return {
|
||||
status: Status.FatalError,
|
||||
result:
|
||||
"Fatal Error: target chain " +
|
||||
transferPayload.targetChain +
|
||||
" is invalid, this is a program bug!",
|
||||
};
|
||||
}
|
||||
return { status: Status.FatalError, result: "ERROR: Invalid payload type" };
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
import { ChainId } from "@certusone/wormhole-sdk";
|
||||
import { ScopedLogger } from "../helpers/logHelper";
|
||||
import { PromHelper } from "../helpers/promHelpers";
|
||||
import {
|
||||
RelayResult,
|
||||
StoreKey,
|
||||
StorePayload,
|
||||
WorkerInfo,
|
||||
} from "../helpers/redisHelper";
|
||||
|
||||
export const REDIS_RETRY_MS = 10 * 1000;
|
||||
export const AUDIT_INTERVAL_MS = 30 * 1000;
|
||||
|
||||
/** TypedFilter is used by subscribeSignedVAA to filter messages returned by the guardian spy */
|
||||
export interface TypedFilter {
|
||||
emitterFilter: { chainId: ChainId; emitterAddress: string };
|
||||
}
|
||||
|
||||
/** Listen to VAAs via a http listener or guardian spy service */
|
||||
export interface Listener {
|
||||
logger: ScopedLogger;
|
||||
|
||||
/** Get filters for the guardian spy subscription */
|
||||
getEmitterFilters(): Promise<TypedFilter[]>;
|
||||
|
||||
/** Parse and validate the received VAAs from the spy */
|
||||
validate(rawVaa: Uint8Array): Promise<unknown>;
|
||||
|
||||
/** Process and add the VAA to redis if it is valid */
|
||||
process(rawVaa: Uint8Array): Promise<void>;
|
||||
|
||||
/** Serialize and store a validated VAA in redis for the relayer */
|
||||
store(key: StoreKey, payload: StorePayload): Promise<void>;
|
||||
}
|
||||
|
||||
/** Relayer is an interface for relaying messages across chains */
|
||||
export interface Relayer {
|
||||
/** Parse the payload and return the target chain id for finding workable items*/
|
||||
targetChainId(payload: Buffer): ChainId;
|
||||
|
||||
/** Relay the signed VAA */
|
||||
relay(
|
||||
signedVAA: string,
|
||||
checkOnly: boolean,
|
||||
walletPrivateKey: any,
|
||||
relayLogger: ScopedLogger,
|
||||
metrics: PromHelper
|
||||
): Promise<RelayResult>;
|
||||
|
||||
/** Process the request to relay a message */
|
||||
process(key: string, privKey: any, logger: ScopedLogger, metrics: PromHelper): Promise<void>;
|
||||
|
||||
/** Run an auditor to ensure the relay was not rolled back due to a chain reorg */
|
||||
runAuditor(workerInfo: WorkerInfo, metrics: PromHelper): Promise<void>;
|
||||
}
|
||||
|
||||
/** Backend is the interface necessary to implement for custom relayers */
|
||||
export interface Backend {
|
||||
listener: Listener;
|
||||
relayer: Relayer;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import defaultBackend from "./default";
|
||||
import { Backend } from "./definitions";
|
||||
|
||||
let backend: Backend;
|
||||
export const getBackend: () => Backend = () => {
|
||||
// Use the global one if it is already instantiated
|
||||
if (backend) {
|
||||
return backend;
|
||||
}
|
||||
if (process.env.CUSTOM_BACKEND) {
|
||||
try {
|
||||
backend = require(process.env.CUSTOM_BACKEND);
|
||||
return backend;
|
||||
} catch (e: any) {
|
||||
throw new Error(
|
||||
`Backend specified in CUSTOM_BACKEND is not importable: ${e?.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!backend) {
|
||||
backend = defaultBackend;
|
||||
}
|
||||
return backend;
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
[
|
||||
{
|
||||
"chainId": 1,
|
||||
"chainName": "Solana",
|
||||
"nativeCurrencySymbol": "SOL",
|
||||
"nodeUrl": "http://solana-devnet:8899",
|
||||
"tokenBridgeAddress": "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE",
|
||||
"bridgeAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o",
|
||||
"wrappedAsset": "So11111111111111111111111111111111111111112"
|
||||
},
|
||||
{
|
||||
"chainId": 2,
|
||||
"chainName": "Ethereum",
|
||||
"nativeCurrencySymbol": "ETH",
|
||||
"nodeUrl": "http://eth-devnet:8545",
|
||||
"tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16",
|
||||
"wrappedAsset": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"
|
||||
},
|
||||
{
|
||||
"chainId": 3,
|
||||
"chainName": "Terra",
|
||||
"nativeCurrencySymbol": "LUNA",
|
||||
"nodeUrl": "http://terra-terrad:1317",
|
||||
"tokenBridgeAddress": "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4",
|
||||
"terraName": "localterra",
|
||||
"terraChainId": "localterra",
|
||||
"terraCoin": "uluna",
|
||||
"terraGasPriceUrl": "http://terra-fcd:3060/v1/txs/gas_prices",
|
||||
"isTerraClassic": true
|
||||
},
|
||||
{
|
||||
"chainId": 4,
|
||||
"chainName": "Binance Smart Chain",
|
||||
"nativeCurrencySymbol": "BNB",
|
||||
"nodeUrl": "http://eth-devnet2:8546",
|
||||
"tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16",
|
||||
"wrappedAsset": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"
|
||||
}
|
||||
]
|
|
@ -1,572 +0,0 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
isTerraChain,
|
||||
nativeToHexString,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { getLogger } from "./helpers/logHelper";
|
||||
|
||||
export type SupportedToken = {
|
||||
chainId: ChainId;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export type CommonEnvironment = {
|
||||
logLevel: string;
|
||||
promPort: number;
|
||||
readinessPort?: number;
|
||||
logDir?: string;
|
||||
redisHost: string;
|
||||
redisPort: number;
|
||||
};
|
||||
|
||||
let loggingEnv: CommonEnvironment | undefined = undefined;
|
||||
|
||||
export const getCommonEnvironment: () => CommonEnvironment = () => {
|
||||
if (loggingEnv) {
|
||||
return loggingEnv;
|
||||
} else {
|
||||
const env = createCommonEnvironment();
|
||||
loggingEnv = env;
|
||||
return loggingEnv;
|
||||
}
|
||||
};
|
||||
|
||||
function createCommonEnvironment(): CommonEnvironment {
|
||||
let logLevel;
|
||||
let promPort;
|
||||
let readinessPort;
|
||||
let logDir;
|
||||
let redisHost;
|
||||
let redisPort;
|
||||
|
||||
if (!process.env.LOG_LEVEL) {
|
||||
throw new Error("Missing required environment variable: LOG_LEVEL");
|
||||
} else {
|
||||
logLevel = process.env.LOG_LEVEL;
|
||||
}
|
||||
|
||||
if (!process.env.LOG_DIR) {
|
||||
//Not mandatory
|
||||
} else {
|
||||
logDir = process.env.LOG_DIR;
|
||||
}
|
||||
|
||||
if (!process.env.PROM_PORT) {
|
||||
throw new Error("Missing required environment variable: PROM_PORT");
|
||||
} else {
|
||||
promPort = parseInt(process.env.PROM_PORT);
|
||||
}
|
||||
|
||||
if (!process.env.READINESS_PORT) {
|
||||
//do nothing
|
||||
} else {
|
||||
readinessPort = parseInt(process.env.READINESS_PORT);
|
||||
}
|
||||
|
||||
if (!process.env.REDIS_HOST) {
|
||||
throw new Error("Missing required environment variable: REDIS_HOST");
|
||||
} else {
|
||||
redisHost = process.env.REDIS_HOST;
|
||||
}
|
||||
|
||||
if (!process.env.REDIS_PORT) {
|
||||
throw new Error("Missing required environment variable: REDIS_PORT");
|
||||
} else {
|
||||
redisPort = parseInt(process.env.REDIS_PORT);
|
||||
}
|
||||
|
||||
return { logLevel, promPort, readinessPort, logDir, redisHost, redisPort };
|
||||
}
|
||||
|
||||
export type RelayerEnvironment = {
|
||||
supportedChains: ChainConfigInfo[];
|
||||
redisHost: string;
|
||||
redisPort: number;
|
||||
clearRedisOnInit: boolean;
|
||||
demoteWorkingOnInit: boolean;
|
||||
supportedTokens: { chainId: ChainId; address: string }[];
|
||||
};
|
||||
|
||||
export type ChainConfigInfo = {
|
||||
chainId: ChainId;
|
||||
chainName: string;
|
||||
nativeCurrencySymbol: string;
|
||||
nodeUrl: string;
|
||||
tokenBridgeAddress: string;
|
||||
walletPrivateKey?: string[];
|
||||
solanaPrivateKey?: Uint8Array[];
|
||||
bridgeAddress?: string;
|
||||
terraName?: string;
|
||||
terraChainId?: string;
|
||||
terraCoin?: string;
|
||||
terraGasPriceUrl?: string;
|
||||
wrappedAsset?: string | null;
|
||||
isTerraClassic?: boolean;
|
||||
};
|
||||
|
||||
export type ListenerEnvironment = {
|
||||
spyServiceHost: string;
|
||||
spyServiceFilters: { chainId: ChainId; emitterAddress: string }[];
|
||||
restPort: number;
|
||||
numSpyWorkers: number;
|
||||
supportedTokens: { chainId: ChainId; address: string }[];
|
||||
};
|
||||
|
||||
let listenerEnv: ListenerEnvironment | undefined = undefined;
|
||||
|
||||
export const getListenerEnvironment: () => ListenerEnvironment = () => {
|
||||
if (listenerEnv) {
|
||||
return listenerEnv;
|
||||
} else {
|
||||
const env = createListenerEnvironment();
|
||||
listenerEnv = env;
|
||||
return listenerEnv;
|
||||
}
|
||||
};
|
||||
|
||||
const createListenerEnvironment: () => ListenerEnvironment = () => {
|
||||
let spyServiceHost: string;
|
||||
let spyServiceFilters: { chainId: ChainId; emitterAddress: string }[] = [];
|
||||
let restPort: number;
|
||||
let numSpyWorkers: number;
|
||||
let supportedTokens: { chainId: ChainId; address: string }[] = [];
|
||||
const logger = getLogger();
|
||||
|
||||
if (!process.env.SPY_SERVICE_HOST) {
|
||||
throw new Error("Missing required environment variable: SPY_SERVICE_HOST");
|
||||
} else {
|
||||
spyServiceHost = process.env.SPY_SERVICE_HOST;
|
||||
}
|
||||
|
||||
logger.info("Getting SPY_SERVICE_FILTERS...");
|
||||
if (!process.env.SPY_SERVICE_FILTERS) {
|
||||
throw new Error(
|
||||
"Missing required environment variable: SPY_SERVICE_FILTERS"
|
||||
);
|
||||
} else {
|
||||
const array = JSON.parse(process.env.SPY_SERVICE_FILTERS);
|
||||
// if (!array.foreach) {
|
||||
if (!array || !Array.isArray(array)) {
|
||||
throw new Error("Spy service filters is not an array.");
|
||||
} else {
|
||||
array.forEach((filter: any) => {
|
||||
if (filter.chainId && filter.emitterAddress) {
|
||||
logger.info(
|
||||
"nativeToHexString: " +
|
||||
nativeToHexString(filter.emitterAddress, filter.chainId)
|
||||
);
|
||||
spyServiceFilters.push({
|
||||
chainId: filter.chainId as ChainId,
|
||||
emitterAddress: filter.emitterAddress,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Invalid filter record. " + filter.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Getting REST_PORT...");
|
||||
if (!process.env.REST_PORT) {
|
||||
throw new Error("Missing required environment variable: REST_PORT");
|
||||
} else {
|
||||
restPort = parseInt(process.env.REST_PORT);
|
||||
}
|
||||
|
||||
logger.info("Getting SPY_NUM_WORKERS...");
|
||||
if (!process.env.SPY_NUM_WORKERS) {
|
||||
throw new Error("Missing required environment variable: SPY_NUM_WORKERS");
|
||||
} else {
|
||||
numSpyWorkers = parseInt(process.env.SPY_NUM_WORKERS);
|
||||
}
|
||||
|
||||
logger.info("Getting SUPPORTED_TOKENS...");
|
||||
if (!process.env.SUPPORTED_TOKENS) {
|
||||
throw new Error("Missing required environment variable: SUPPORTED_TOKENS");
|
||||
} else {
|
||||
// const array = JSON.parse(process.env.SUPPORTED_TOKENS);
|
||||
const array = eval(process.env.SUPPORTED_TOKENS);
|
||||
if (!array || !Array.isArray(array)) {
|
||||
throw new Error("SUPPORTED_TOKENS is not an array.");
|
||||
} else {
|
||||
array.forEach((token: any) => {
|
||||
if (token.chainId && token.address) {
|
||||
supportedTokens.push({
|
||||
chainId: token.chainId,
|
||||
address: token.address,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Invalid token record. " + token.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Setting the listener backend...");
|
||||
|
||||
return {
|
||||
spyServiceHost,
|
||||
spyServiceFilters,
|
||||
restPort,
|
||||
numSpyWorkers,
|
||||
supportedTokens,
|
||||
};
|
||||
};
|
||||
|
||||
let relayerEnv: RelayerEnvironment | undefined = undefined;
|
||||
|
||||
export const getRelayerEnvironment: () => RelayerEnvironment = () => {
|
||||
if (relayerEnv) {
|
||||
return relayerEnv;
|
||||
} else {
|
||||
const env = createRelayerEnvironment();
|
||||
relayerEnv = env;
|
||||
return relayerEnv;
|
||||
}
|
||||
};
|
||||
|
||||
const createRelayerEnvironment: () => RelayerEnvironment = () => {
|
||||
let supportedChains: ChainConfigInfo[] = [];
|
||||
let redisHost: string;
|
||||
let redisPort: number;
|
||||
let clearRedisOnInit: boolean;
|
||||
let demoteWorkingOnInit: boolean;
|
||||
let supportedTokens: { chainId: ChainId; address: string }[] = [];
|
||||
const logger = getLogger();
|
||||
|
||||
if (!process.env.REDIS_HOST) {
|
||||
throw new Error("Missing required environment variable: REDIS_HOST");
|
||||
} else {
|
||||
redisHost = process.env.REDIS_HOST;
|
||||
}
|
||||
|
||||
if (!process.env.REDIS_PORT) {
|
||||
throw new Error("Missing required environment variable: REDIS_PORT");
|
||||
} else {
|
||||
redisPort = parseInt(process.env.REDIS_PORT);
|
||||
}
|
||||
|
||||
if (process.env.CLEAR_REDIS_ON_INIT === undefined) {
|
||||
throw new Error(
|
||||
"Missing required environment variable: CLEAR_REDIS_ON_INIT"
|
||||
);
|
||||
} else {
|
||||
if (process.env.CLEAR_REDIS_ON_INIT.toLowerCase() === "true") {
|
||||
clearRedisOnInit = true;
|
||||
} else {
|
||||
clearRedisOnInit = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.DEMOTE_WORKING_ON_INIT === undefined) {
|
||||
throw new Error(
|
||||
"Missing required environment variable: DEMOTE_WORKING_ON_INIT"
|
||||
);
|
||||
} else {
|
||||
if (process.env.DEMOTE_WORKING_ON_INIT.toLowerCase() === "true") {
|
||||
demoteWorkingOnInit = true;
|
||||
} else {
|
||||
demoteWorkingOnInit = false;
|
||||
}
|
||||
}
|
||||
|
||||
supportedChains = loadChainConfig();
|
||||
|
||||
if (!process.env.SUPPORTED_TOKENS) {
|
||||
throw new Error("Missing required environment variable: SUPPORTED_TOKENS");
|
||||
} else {
|
||||
// const array = JSON.parse(process.env.SUPPORTED_TOKENS);
|
||||
const array = eval(process.env.SUPPORTED_TOKENS);
|
||||
if (!array || !Array.isArray(array)) {
|
||||
throw new Error("SUPPORTED_TOKENS is not an array.");
|
||||
} else {
|
||||
array.forEach((token: any) => {
|
||||
if (token.chainId && token.address) {
|
||||
supportedTokens.push({
|
||||
chainId: token.chainId,
|
||||
address: token.address,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Invalid token record. " + token.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Setting the relayer backend...");
|
||||
|
||||
return {
|
||||
supportedChains,
|
||||
redisHost,
|
||||
redisPort,
|
||||
clearRedisOnInit,
|
||||
demoteWorkingOnInit,
|
||||
supportedTokens,
|
||||
};
|
||||
};
|
||||
|
||||
//Polygon is not supported on local Tilt network atm.
|
||||
export function loadChainConfig(): ChainConfigInfo[] {
|
||||
if (!process.env.SUPPORTED_CHAINS) {
|
||||
throw new Error("Missing required environment variable: SUPPORTED_CHAINS");
|
||||
}
|
||||
if (!process.env.PRIVATE_KEYS) {
|
||||
throw new Error("Missing required environment variable: PRIVATE_KEYS");
|
||||
}
|
||||
|
||||
const unformattedChains = JSON.parse(process.env.SUPPORTED_CHAINS);
|
||||
const unformattedPrivateKeys = JSON.parse(process.env.PRIVATE_KEYS);
|
||||
const supportedChains: ChainConfigInfo[] = [];
|
||||
|
||||
if (!unformattedChains.forEach) {
|
||||
throw new Error("SUPPORTED_CHAINS arg was not an array.");
|
||||
}
|
||||
if (!unformattedPrivateKeys.forEach) {
|
||||
throw new Error("PRIVATE_KEYS arg was not an array.");
|
||||
}
|
||||
|
||||
unformattedChains.forEach((element: any) => {
|
||||
if (!element.chainId) {
|
||||
throw new Error("Invalid chain config: " + element);
|
||||
}
|
||||
|
||||
const privateKeyObj = unformattedPrivateKeys.find(
|
||||
(x: any) => x.chainId === element.chainId
|
||||
);
|
||||
if (!privateKeyObj) {
|
||||
throw new Error(
|
||||
"Failed to find private key object for configured chain ID: " +
|
||||
element.chainId
|
||||
);
|
||||
}
|
||||
|
||||
if (element.chainId === CHAIN_ID_SOLANA) {
|
||||
supportedChains.push(
|
||||
createSolanaChainConfig(element, privateKeyObj.privateKeys)
|
||||
);
|
||||
} else if (isTerraChain(element.chainId)) {
|
||||
supportedChains.push(
|
||||
createTerraChainConfig(element, privateKeyObj.privateKeys)
|
||||
);
|
||||
} else {
|
||||
supportedChains.push(
|
||||
createEvmChainConfig(element, privateKeyObj.privateKeys)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return supportedChains;
|
||||
}
|
||||
|
||||
function createSolanaChainConfig(
|
||||
config: any,
|
||||
privateKeys: any[]
|
||||
): ChainConfigInfo {
|
||||
let chainId: ChainId;
|
||||
let chainName: string;
|
||||
let nativeCurrencySymbol: string;
|
||||
let nodeUrl: string;
|
||||
let tokenBridgeAddress: string;
|
||||
let solanaPrivateKey: Uint8Array[] = [];
|
||||
let bridgeAddress: string;
|
||||
let wrappedAsset: string | null;
|
||||
|
||||
if (!config.chainId) {
|
||||
throw new Error("Missing required field in chain config: chainId");
|
||||
}
|
||||
if (!config.chainName) {
|
||||
throw new Error("Missing required field in chain config: chainName");
|
||||
}
|
||||
if (!config.nativeCurrencySymbol) {
|
||||
throw new Error(
|
||||
"Missing required field in chain config: nativeCurrencySymbol"
|
||||
);
|
||||
}
|
||||
if (!config.nodeUrl) {
|
||||
throw new Error("Missing required field in chain config: nodeUrl");
|
||||
}
|
||||
if (!config.tokenBridgeAddress) {
|
||||
throw new Error(
|
||||
"Missing required field in chain config: tokenBridgeAddress"
|
||||
);
|
||||
}
|
||||
if (!(privateKeys && privateKeys.length && privateKeys.forEach)) {
|
||||
throw new Error(
|
||||
"Ill formatted object received as private keys for Solana."
|
||||
);
|
||||
}
|
||||
if (!config.bridgeAddress) {
|
||||
throw new Error("Missing required field in chain config: bridgeAddress");
|
||||
}
|
||||
if (!config.wrappedAsset) {
|
||||
throw new Error("Missing required field in chain config: wrappedAsset");
|
||||
}
|
||||
|
||||
chainId = config.chainId;
|
||||
chainName = config.chainName;
|
||||
nativeCurrencySymbol = config.nativeCurrencySymbol;
|
||||
nodeUrl = config.nodeUrl;
|
||||
tokenBridgeAddress = config.tokenBridgeAddress;
|
||||
bridgeAddress = config.bridgeAddress;
|
||||
wrappedAsset = config.wrappedAsset;
|
||||
|
||||
privateKeys.forEach((item: any) => {
|
||||
try {
|
||||
const uint = Uint8Array.from(item);
|
||||
solanaPrivateKey.push(uint);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
"Failed to coerce Solana private keys into a uint array. ENV JSON is possibly incorrect."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
chainId,
|
||||
chainName,
|
||||
nativeCurrencySymbol,
|
||||
nodeUrl,
|
||||
tokenBridgeAddress,
|
||||
bridgeAddress,
|
||||
solanaPrivateKey,
|
||||
wrappedAsset,
|
||||
};
|
||||
}
|
||||
|
||||
function createTerraChainConfig(
|
||||
config: any,
|
||||
privateKeys: any[]
|
||||
): ChainConfigInfo {
|
||||
let chainId: ChainId;
|
||||
let chainName: string;
|
||||
let nativeCurrencySymbol: string;
|
||||
let nodeUrl: string;
|
||||
let tokenBridgeAddress: string;
|
||||
let walletPrivateKey: string[];
|
||||
let terraName: string;
|
||||
let terraChainId: string;
|
||||
let terraCoin: string;
|
||||
let terraGasPriceUrl: string;
|
||||
let isTerraClassic = false;
|
||||
|
||||
if (!config.chainId) {
|
||||
throw new Error("Missing required field in chain config: chainId");
|
||||
}
|
||||
if (!config.chainName) {
|
||||
throw new Error("Missing required field in chain config: chainName");
|
||||
}
|
||||
if (!config.nativeCurrencySymbol) {
|
||||
throw new Error(
|
||||
"Missing required field in chain config: nativeCurrencySymbol"
|
||||
);
|
||||
}
|
||||
if (!config.nodeUrl) {
|
||||
throw new Error("Missing required field in chain config: nodeUrl");
|
||||
}
|
||||
if (!config.tokenBridgeAddress) {
|
||||
throw new Error(
|
||||
"Missing required field in chain config: tokenBridgeAddress"
|
||||
);
|
||||
}
|
||||
if (!(privateKeys && privateKeys.length && privateKeys.forEach)) {
|
||||
throw new Error("Private keys for Terra are length zero or not an array.");
|
||||
}
|
||||
if (!config.terraName) {
|
||||
throw new Error("Missing required field in chain config: terraName");
|
||||
}
|
||||
if (!config.terraChainId) {
|
||||
throw new Error("Missing required field in chain config: terraChainId");
|
||||
}
|
||||
if (!config.terraCoin) {
|
||||
throw new Error("Missing required field in chain config: terraCoin");
|
||||
}
|
||||
if (!config.terraGasPriceUrl) {
|
||||
throw new Error("Missing required field in chain config: terraGasPriceUrl");
|
||||
}
|
||||
|
||||
chainId = config.chainId;
|
||||
chainName = config.chainName;
|
||||
nativeCurrencySymbol = config.nativeCurrencySymbol;
|
||||
nodeUrl = config.nodeUrl;
|
||||
tokenBridgeAddress = config.tokenBridgeAddress;
|
||||
walletPrivateKey = privateKeys;
|
||||
terraName = config.terraName;
|
||||
terraChainId = config.terraChainId;
|
||||
terraCoin = config.terraCoin;
|
||||
terraGasPriceUrl = config.terraGasPriceUrl;
|
||||
isTerraClassic = config.isTerraClassic || false;
|
||||
|
||||
return {
|
||||
chainId,
|
||||
chainName,
|
||||
nativeCurrencySymbol,
|
||||
nodeUrl,
|
||||
tokenBridgeAddress,
|
||||
walletPrivateKey,
|
||||
terraName,
|
||||
terraChainId,
|
||||
terraCoin,
|
||||
terraGasPriceUrl,
|
||||
isTerraClassic,
|
||||
};
|
||||
}
|
||||
|
||||
function createEvmChainConfig(
|
||||
config: any,
|
||||
privateKeys: any[]
|
||||
): ChainConfigInfo {
|
||||
let chainId: ChainId;
|
||||
let chainName: string;
|
||||
let nativeCurrencySymbol: string;
|
||||
let nodeUrl: string;
|
||||
let tokenBridgeAddress: string;
|
||||
let walletPrivateKey: string[];
|
||||
let wrappedAsset: string;
|
||||
|
||||
if (!config.chainId) {
|
||||
throw new Error("Missing required field in chain config: chainId");
|
||||
}
|
||||
if (!config.chainName) {
|
||||
throw new Error("Missing required field in chain config: chainName");
|
||||
}
|
||||
if (!config.nativeCurrencySymbol) {
|
||||
throw new Error(
|
||||
"Missing required field in chain config: nativeCurrencySymbol"
|
||||
);
|
||||
}
|
||||
if (!config.nodeUrl) {
|
||||
throw new Error("Missing required field in chain config: nodeUrl");
|
||||
}
|
||||
if (!config.tokenBridgeAddress) {
|
||||
throw new Error(
|
||||
"Missing required field in chain config: tokenBridgeAddress"
|
||||
);
|
||||
}
|
||||
if (!(privateKeys && privateKeys.length && privateKeys.forEach)) {
|
||||
throw new Error(
|
||||
`Private keys for chain id ${config.chainId} are length zero or not an array.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.wrappedAsset) {
|
||||
throw new Error("Missing required field in chain config: wrappedAsset");
|
||||
}
|
||||
chainId = config.chainId;
|
||||
chainName = config.chainName;
|
||||
nativeCurrencySymbol = config.nativeCurrencySymbol;
|
||||
nodeUrl = config.nodeUrl;
|
||||
tokenBridgeAddress = config.tokenBridgeAddress;
|
||||
walletPrivateKey = privateKeys;
|
||||
wrappedAsset = config.wrappedAsset;
|
||||
|
||||
return {
|
||||
chainId,
|
||||
chainName,
|
||||
nativeCurrencySymbol,
|
||||
nodeUrl,
|
||||
tokenBridgeAddress,
|
||||
walletPrivateKey,
|
||||
wrappedAsset,
|
||||
};
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { config } from "dotenv";
|
||||
const configFile: string = process.env.SPY_RELAY_CONFIG
|
||||
? process.env.SPY_RELAY_CONFIG
|
||||
: ".env.sample";
|
||||
console.log("loading config file [%s]", configFile);
|
||||
config({ path: configFile });
|
||||
export {};
|
|
@ -1,45 +0,0 @@
|
|||
import { beforeAll, test } from "@jest/globals";
|
||||
import { getLogger, getScopedLogger } from "./logHelper";
|
||||
|
||||
// TODO: mock and confirm output
|
||||
|
||||
beforeAll(() => {
|
||||
require("./loadConfig");
|
||||
process.env.LOG_DIR = ".";
|
||||
});
|
||||
|
||||
test("should log default logs", () => {
|
||||
const logger = getLogger();
|
||||
logger.info("test");
|
||||
});
|
||||
test("should use child labels", () => {
|
||||
getLogger().child({}).info("test without labels");
|
||||
getLogger().child({ labels: [] }).info("test with empty labels");
|
||||
getLogger()
|
||||
.child({ labels: ["one"] })
|
||||
.info("test with one label");
|
||||
getLogger()
|
||||
.child({ labels: ["one", "two"] })
|
||||
.info("test with two labels");
|
||||
getLogger()
|
||||
.child({ labels: ["one", "two", "three"] })
|
||||
.info("test with three labels");
|
||||
});
|
||||
test("should allow child label override", () => {
|
||||
const root = getLogger();
|
||||
const parent = root.child({ labels: ["override-me"] });
|
||||
const child = root.child({ labels: ["overridden"] });
|
||||
root.info("root log");
|
||||
parent.info("parent log");
|
||||
child.info("child log");
|
||||
});
|
||||
test("scoped logger", () => {
|
||||
getScopedLogger([]).info("no labels");
|
||||
getScopedLogger(["one"]).info("one label");
|
||||
});
|
||||
test("scoped logger inheritance", () => {
|
||||
const parent = getScopedLogger(["parent"]);
|
||||
const child = getScopedLogger(["child"], parent);
|
||||
parent.info("parent log");
|
||||
child.info("child log");
|
||||
});
|
|
@ -1,91 +0,0 @@
|
|||
import winston = require("winston");
|
||||
import { getCommonEnvironment } from "../configureEnv";
|
||||
|
||||
//Be careful not to access this before having called init logger, or it will be undefined
|
||||
let logger: winston.Logger | undefined;
|
||||
|
||||
export function getLogger(): winston.Logger {
|
||||
if (logger) {
|
||||
return logger;
|
||||
} else {
|
||||
logger = initLogger();
|
||||
return logger;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ScopedLogger extends winston.Logger {
|
||||
scope?: string[];
|
||||
}
|
||||
|
||||
// Child loggers can't override defaultMeta, they add their own defaultRequestMetadata
|
||||
// ...which is stored in a closure we can't read, so we extend it ourselves :)
|
||||
// https://github.com/winstonjs/winston/blob/a320b0cf7f3c550a354ce4264d7634ebc60b0a67/lib/winston/logger.js#L45
|
||||
export function getScopedLogger(
|
||||
labels: string[],
|
||||
parentLogger?: ScopedLogger
|
||||
): ScopedLogger {
|
||||
const scope = [...(parentLogger?.scope || []), ...labels];
|
||||
const logger = parentLogger || getLogger();
|
||||
const child: ScopedLogger = logger.child({
|
||||
labels: scope,
|
||||
});
|
||||
child.scope = scope;
|
||||
return child;
|
||||
}
|
||||
|
||||
function initLogger(): winston.Logger {
|
||||
const loggingEnv = getCommonEnvironment();
|
||||
|
||||
let useConsole = true;
|
||||
let logFileName;
|
||||
if (loggingEnv.logDir) {
|
||||
useConsole = false;
|
||||
logFileName =
|
||||
loggingEnv.logDir + "/spy_relay." + new Date().toISOString() + ".log";
|
||||
}
|
||||
|
||||
let logLevel = loggingEnv.logLevel || "info";
|
||||
|
||||
let transport: any;
|
||||
if (useConsole) {
|
||||
console.log("spy_relay is logging to the console at level [%s]", logLevel);
|
||||
|
||||
transport = new winston.transports.Console({
|
||||
level: logLevel,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
"spy_relay is logging to [%s] at level [%s]",
|
||||
logFileName,
|
||||
logLevel
|
||||
);
|
||||
|
||||
transport = new winston.transports.File({
|
||||
filename: logFileName,
|
||||
level: logLevel,
|
||||
});
|
||||
}
|
||||
|
||||
const logConfiguration: winston.LoggerOptions = {
|
||||
// NOTE: do not specify labels in defaultMeta, as it cannot be overridden
|
||||
transports: [transport],
|
||||
format: winston.format.combine(
|
||||
winston.format.splat(),
|
||||
winston.format.simple(),
|
||||
winston.format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss.SSS",
|
||||
}),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.printf(
|
||||
(info: any) =>
|
||||
`${[info.timestamp]}|${info.level}|${
|
||||
info.labels && info.labels.length > 0
|
||||
? info.labels.join("|")
|
||||
: "main"
|
||||
}: ${info.message}`
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
return winston.createLogger(logConfiguration);
|
||||
}
|
|
@ -1,236 +0,0 @@
|
|||
import { ChainId } from "@certusone/wormhole-sdk";
|
||||
import http = require("http");
|
||||
import client = require("prom-client");
|
||||
import { WalletBalance } from "../monitor/walletMonitor";
|
||||
import { chainIDStrings } from "../utils/wormhole";
|
||||
import { getScopedLogger } from "./logHelper";
|
||||
import { RedisTables } from "./redisHelper";
|
||||
|
||||
// NOTE: To create a new metric:
|
||||
// 1) Create a private counter/gauge with appropriate name and help
|
||||
// 2) Create a method to set the metric to a value
|
||||
// 3) Register the metric
|
||||
|
||||
const logger = getScopedLogger(["prometheusHelpers"]);
|
||||
export enum PromMode {
|
||||
Listen,
|
||||
Relay,
|
||||
WalletMonitor,
|
||||
All,
|
||||
}
|
||||
|
||||
export class PromHelper {
|
||||
private _register = new client.Registry();
|
||||
private _mode: PromMode;
|
||||
private collectDefaultMetrics = client.collectDefaultMetrics;
|
||||
|
||||
// Actual metrics (please prefix all metrics with `spy_relay_`)
|
||||
private successCounter = new client.Counter({
|
||||
name: "spy_relay_successes",
|
||||
help: "number of successful relays",
|
||||
labelNames: ["chain_name"],
|
||||
});
|
||||
private confirmedCounter = new client.Counter({
|
||||
name: "spy_relay_confirmed_successes",
|
||||
help: "number of confirmed successful relays",
|
||||
labelNames: ["chain_name"],
|
||||
});
|
||||
private failureCounter = new client.Counter({
|
||||
name: "spy_relay_failures",
|
||||
help: "number of failed relays",
|
||||
labelNames: ["chain_name"],
|
||||
});
|
||||
private rollbackCounter = new client.Counter({
|
||||
name: "spy_relay_rollback",
|
||||
help: "number of rolled back relays",
|
||||
labelNames: ["chain_name"],
|
||||
});
|
||||
private completeTime = new client.Histogram({
|
||||
name: "spy_relay_complete_time",
|
||||
help: "Time is took to complete transfer",
|
||||
buckets: [400, 800, 1600, 3200, 6400, 12800],
|
||||
});
|
||||
private listenCounter = new client.Counter({
|
||||
name: "spy_relay_VAAs_received",
|
||||
help: "number of VAAs received",
|
||||
});
|
||||
private alreadyExecutedCounter = new client.Counter({
|
||||
name: "spy_relay_already_executed",
|
||||
help: "number of transfers rejected due to already having been executed",
|
||||
});
|
||||
private listenerMemqueue = new client.Gauge({
|
||||
name: "spy_relay_listener_memqueue_length",
|
||||
help: "number of items in memory in the listener waiting to be pushed to redis.",
|
||||
});
|
||||
private redisQueue = new client.Gauge({
|
||||
name: "spy_relay_redis_queue_length",
|
||||
help: "number of items in the pending queue.",
|
||||
labelNames: ["queue", "source_chain_name", "target_chain_name"],
|
||||
});
|
||||
|
||||
// Wallet metrics
|
||||
private walletBalance = new client.Gauge({
|
||||
name: "spy_relay_wallet_balance",
|
||||
help: "Wallet balance for a supported token",
|
||||
labelNames: [
|
||||
"currency",
|
||||
"chain_name",
|
||||
"wallet",
|
||||
"currency_address",
|
||||
"is_native",
|
||||
],
|
||||
});
|
||||
// End metrics
|
||||
|
||||
private server = http.createServer(async (req, res) => {
|
||||
// GKE's ingress-gce doesn't support custom URLs for healthchecks
|
||||
// without some stupid, so return 200 on / for prometheus to make
|
||||
// it happy.
|
||||
if (req.url === "/") {
|
||||
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||
res.write("ok");
|
||||
res.end();
|
||||
// The gke ingress-gce does not support stripping path prefixes
|
||||
} else if (
|
||||
req.url === "/metrics" ||
|
||||
req.url === "/relayer" ||
|
||||
req.url === "/listener" ||
|
||||
req.url === "/wallet-monitor"
|
||||
) {
|
||||
// Return all metrics in the Prometheus exposition format
|
||||
res.setHeader("Content-Type", this._register.contentType);
|
||||
res.end(await this._register.metrics());
|
||||
} else {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.write("404 Not Found - " + req.url + "\n");
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
constructor(name: string, port: number, mode: PromMode) {
|
||||
var mode_name: string = "";
|
||||
// Human readable mode name for the metrics
|
||||
if (mode === PromMode.Listen) {
|
||||
mode_name = "listener";
|
||||
} else if (mode === PromMode.Relay) {
|
||||
mode_name = "relayer";
|
||||
} else if (mode === PromMode.WalletMonitor) {
|
||||
mode_name = "wallet-monitor";
|
||||
} else if (mode === PromMode.All) {
|
||||
mode_name = "all";
|
||||
}
|
||||
|
||||
this._register.setDefaultLabels({
|
||||
app: name,
|
||||
mode: mode_name,
|
||||
});
|
||||
// Uncomment to collect the default metrics (cpu/memory/nodejs gc stuff/etc)
|
||||
//this.collectDefaultMetrics({ register: this._register, prefix: "spy_relayer_" });
|
||||
|
||||
this._mode = mode;
|
||||
// Register each metric
|
||||
if (this._mode === PromMode.Listen || this._mode === PromMode.All) {
|
||||
this._register.registerMetric(this.listenCounter);
|
||||
}
|
||||
if (this._mode === PromMode.Relay || this._mode === PromMode.All) {
|
||||
this._register.registerMetric(this.successCounter);
|
||||
this._register.registerMetric(this.confirmedCounter);
|
||||
this._register.registerMetric(this.failureCounter);
|
||||
this._register.registerMetric(this.rollbackCounter);
|
||||
this._register.registerMetric(this.alreadyExecutedCounter);
|
||||
this._register.registerMetric(this.redisQueue);
|
||||
}
|
||||
if (this._mode === PromMode.WalletMonitor || this._mode === PromMode.All) {
|
||||
this._register.registerMetric(this.walletBalance);
|
||||
}
|
||||
// End registering metric
|
||||
|
||||
this.server.listen(port);
|
||||
}
|
||||
|
||||
// These are the accessor methods for the metrics
|
||||
incSuccesses(chainId: ChainId, value?: number) {
|
||||
this.successCounter
|
||||
.labels({ chain_name: chainIDStrings[chainId] || "Unknown" })
|
||||
.inc(value);
|
||||
}
|
||||
incConfirmed(chainId: ChainId, value?: number) {
|
||||
this.confirmedCounter
|
||||
.labels({ chain_name: chainIDStrings[chainId] || "Unknown" })
|
||||
.inc(value);
|
||||
}
|
||||
incFailures(chainId: ChainId, value?: number) {
|
||||
this.failureCounter
|
||||
.labels({ chain_name: chainIDStrings[chainId] || "Unknown" })
|
||||
.inc(value);
|
||||
}
|
||||
incRollback(chainId: ChainId, value?: number) {
|
||||
this.rollbackCounter
|
||||
.labels({ chain_name: chainIDStrings[chainId] || "Unknown" })
|
||||
.inc(value);
|
||||
}
|
||||
addCompleteTime(val: number) {
|
||||
this.completeTime.observe(val);
|
||||
}
|
||||
incIncoming() {
|
||||
this.listenCounter.inc();
|
||||
}
|
||||
incAlreadyExec() {
|
||||
this.alreadyExecutedCounter.inc();
|
||||
}
|
||||
|
||||
handleListenerMemqueue(size: number) {
|
||||
this.listenerMemqueue.set(size);
|
||||
}
|
||||
setRedisQueue(
|
||||
queue: RedisTables,
|
||||
sourceChainId: ChainId,
|
||||
targetChainId: ChainId,
|
||||
size: number
|
||||
) {
|
||||
this.redisQueue
|
||||
.labels({
|
||||
queue: RedisTables[queue].toLowerCase(),
|
||||
source_chain_name: chainIDStrings[sourceChainId],
|
||||
target_chain_name: chainIDStrings[targetChainId],
|
||||
})
|
||||
.set(size);
|
||||
}
|
||||
|
||||
// Wallet metrics
|
||||
handleWalletBalances(balances: WalletBalance[]) {
|
||||
const scopedLogger = getScopedLogger(["handleWalletBalances"], logger);
|
||||
// Walk through each wallet
|
||||
// create a gauge for the balance
|
||||
// set the gauge
|
||||
//this.walletMetrics = [];
|
||||
for (const bal of balances) {
|
||||
try {
|
||||
if (bal.currencyName.length === 0) {
|
||||
bal.currencyName = "UNK";
|
||||
}
|
||||
let formBal: number;
|
||||
if (!bal.balanceFormatted) {
|
||||
formBal = 0;
|
||||
} else {
|
||||
formBal = parseFloat(bal.balanceFormatted);
|
||||
}
|
||||
this.walletBalance
|
||||
.labels({
|
||||
currency: bal.currencyName,
|
||||
chain_name: chainIDStrings[bal.chainId] || "Unknown",
|
||||
wallet: bal.walletAddress,
|
||||
currency_address: bal.currencyAddressNative,
|
||||
is_native: bal.isNative ? "1" : "0",
|
||||
})
|
||||
.set(formBal);
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
scopedLogger.error("Caught error: " + e.message);
|
||||
} else {
|
||||
scopedLogger.error("Caught error: %o", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
process.env.LOG_LEVEL = "debug";
|
||||
process.env.PROM_PORT = "0";
|
||||
process.env.REDIS_HOST = "localhost";
|
||||
process.env.REDIS_PORT = "0";
|
||||
|
||||
import {
|
||||
ChainId,
|
||||
importCoreWasm,
|
||||
setDefaultWasm,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { chainIDStrings } from "../utils/wormhole";
|
||||
import {
|
||||
createSourceToTargetMap,
|
||||
incrementSourceToTargetMap,
|
||||
} from "./redisHelper";
|
||||
const TEST_KEY = `{"chain_id":3,"emitter_address":"0000000000000000000000007cf7b764e38a0a5e967972c1df77d432510564e2","sequence":77391}`;
|
||||
const TEST_VAA_BYTES =
|
||||
"01000000010d00d37d5af819b2230d7c2b0ad059d03f0410ee01fa05fba3ede9c180004d6e4cb36b8e4383318422a63705451632b3adc3ca85839e23e2c15408eb21e32e5dbbd20002a31149de339b417fbd9e06fdfb9644f48c3b3981811b170556785517f44316c4171367221ba4f3a0a756115c27fef6a636bfb6447862485884500664652bca920103c814d18dddb5816a8310b496d56cacaa9dac294aa25a6c9b4d194df20c5c7ffd22ca2fdbe389e4e05daac51159b2dd73d302eaf9cc9ddc9aa04de2ef4e07dbe3000455f4a08e1a96493129910237dc66db46d20e0baab9a54ee51587651724ddbe1423b4007802505796cace80b992444704af1a3b5f7813055d0beaeba2d93c25b301070b6732602bf0629dfd7ffc71b70900f4ea21ae4a3a03067df685de4f71965b157d6c5e9fcc3f275b64035e307fa71a9d64a1abd213ee00283e8e8c1ed7507d9e000860e4c5539dda95b5a5c3ad82c4fd9023456b095ca9ff1d51e3d3e673ff60805238b70089fdb2e9c00747f9f6d86a5b56bc9a81f3e53fbcb0d0256a1c2be4827e010a532463674858c045328bbcc632df851b0274709eb2bd139401df54fe6d049afe69ca807590c29fe2753b66a84ae1f99209e9e9d273d3a54865691168a9c79f31010b356897c0e0e23c9b3d99cda837fe09b1ef519ed479981473a832791db29b09a31be1cd2d2d64ddb16972d201dc694adea852544df180711d8baa6606f250a27b000c25ec035c97bb0cffcc61cd8b1280c5b03b8080e77a603198bcbffa3fcb946e4924fc201cbd24af179f89779107421edbdb8247bd85984c6b099f0d42611a9695010df05a74cb0924a0952ccf54e1539d6823c828a597176f284697e73ebcb082964f79585affe1f269873ec7eb8b7cf6f605a21fc3db0a22df409c2a30d41a866d00011095bdf09fb178e1ddd950e66e82fcfff99dc220b76f66b51a83513ce4b826ff4b053be0e290c424e4535f5b915fd1992102405c7cc3cffe086b87fccb942c084200118a15aae39395d490b2f5c6fb41e9f7d1ba8594905d62e8250ecc3bc46e71638f4fd35c7e711e30273fd49b8cb349517918eeb8e14885fabf449d10013449497d0012ea54ad52291bac7c031dfb103adf094fc9461758a32a15c93ae02a011126602b09af0a6a389924bd83bb57146962c083040ca6b467dabababb8e8819c277e6e60162586e2900013a2700030000000000000000000000007cf7b764e38a0a5e967972c1df77d432510564e20000000000012e4f000100000000000000000000000000000000000000000000000000000000026271da010000000000000000000000000000000000000000000000000000756c756e610003000000000000000000000000d2499424e5822dc6dadebec9518c1afc1b970be2000a00000000000000000000000000000000000000000000000000000000000017da";
|
||||
test("should correctly increment sourceToTargetMap", async () => {
|
||||
setDefaultWasm("node");
|
||||
const { parse_vaa } = await importCoreWasm();
|
||||
const knownChainIds = Object.keys(chainIDStrings).map(
|
||||
(c) => Number(c) as ChainId
|
||||
);
|
||||
const sourceToTargetMap = createSourceToTargetMap(knownChainIds);
|
||||
const redisClientMock: any = {
|
||||
get: async () => `{"vaa_bytes":"${TEST_VAA_BYTES}"}`,
|
||||
};
|
||||
await incrementSourceToTargetMap(
|
||||
TEST_KEY,
|
||||
redisClientMock,
|
||||
parse_vaa,
|
||||
sourceToTargetMap
|
||||
);
|
||||
expect(sourceToTargetMap[3][1]).toBe(0);
|
||||
expect(sourceToTargetMap[3][10]).toBe(1);
|
||||
});
|
|
@ -1,424 +0,0 @@
|
|||
import {
|
||||
ChainId,
|
||||
hexToUint8Array,
|
||||
importCoreWasm,
|
||||
parseTransferPayload,
|
||||
uint8ArrayToHex,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { createClient, RedisClientType } from "redis";
|
||||
import { getCommonEnvironment } from "../configureEnv";
|
||||
import { ParsedTransferPayload, ParsedVaa } from "../listener/validation";
|
||||
import { chainIDStrings } from "../utils/wormhole";
|
||||
import { getScopedLogger } from "./logHelper";
|
||||
import { PromHelper } from "./promHelpers";
|
||||
import { sleep } from "./utils";
|
||||
|
||||
const logger = getScopedLogger(["redisHelper"]);
|
||||
const commonEnv = getCommonEnvironment();
|
||||
const { redisHost, redisPort } = commonEnv;
|
||||
let promHelper: PromHelper;
|
||||
|
||||
//Module internals
|
||||
const redisMutex = new Mutex();
|
||||
let redisQueue = new Array<[string, string]>();
|
||||
|
||||
export function getBackupQueue() {
|
||||
return redisQueue;
|
||||
}
|
||||
|
||||
export enum RedisTables {
|
||||
INCOMING = 0,
|
||||
WORKING = 1,
|
||||
}
|
||||
|
||||
export function init(ph: PromHelper): boolean {
|
||||
logger.info("will connect to redis at [" + redisHost + ":" + redisPort + "]");
|
||||
promHelper = ph;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function connectToRedis() {
|
||||
let rClient;
|
||||
try {
|
||||
rClient = createClient({
|
||||
socket: {
|
||||
host: redisHost,
|
||||
port: redisPort,
|
||||
},
|
||||
});
|
||||
|
||||
rClient.on("connect", function (err) {
|
||||
if (err) {
|
||||
logger.error(
|
||||
"connectToRedis: failed to connect to host [" +
|
||||
redisHost +
|
||||
"], port [" +
|
||||
redisPort +
|
||||
"]: %o",
|
||||
err
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await rClient.connect();
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"connectToRedis: failed to connect to host [" +
|
||||
redisHost +
|
||||
"], port [" +
|
||||
redisPort +
|
||||
"]: %o",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
return rClient;
|
||||
}
|
||||
|
||||
export async function storeInRedis(name: string, value: string) {
|
||||
if (!name) {
|
||||
logger.error("storeInRedis: missing name");
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
logger.error("storeInRedis: missing value");
|
||||
return;
|
||||
}
|
||||
|
||||
await redisMutex.runExclusive(async () => {
|
||||
logger.debug("storeInRedis: connecting to redis.");
|
||||
let redisClient;
|
||||
try {
|
||||
redisQueue.push([name, value]);
|
||||
redisClient = await connectToRedis();
|
||||
if (!redisClient) {
|
||||
logger.error(
|
||||
"Failed to connect to redis, enqueued vaa, there are now " +
|
||||
redisQueue.length +
|
||||
" enqueued events"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"now connected to redis, attempting to push " +
|
||||
redisQueue.length +
|
||||
" queued items"
|
||||
);
|
||||
for (let item = redisQueue.pop(); item; item = redisQueue.pop()) {
|
||||
await addToRedis(redisClient, item[0], item[1]);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Failed during redis item push. Currently" +
|
||||
redisQueue.length +
|
||||
" enqueued items"
|
||||
);
|
||||
logger.error(
|
||||
"encountered an exception while pushing items to redis %o",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (redisClient) {
|
||||
await redisClient.quit();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Failed to quit redis client");
|
||||
}
|
||||
});
|
||||
|
||||
promHelper.handleListenerMemqueue(redisQueue.length);
|
||||
}
|
||||
|
||||
export async function addToRedis(
|
||||
redisClient: any,
|
||||
name: string,
|
||||
value: string
|
||||
) {
|
||||
try {
|
||||
logger.debug("storeInRedis: storing in redis. name: " + name);
|
||||
await redisClient.select(RedisTables.INCOMING);
|
||||
await redisClient.set(name, value);
|
||||
|
||||
logger.debug("storeInRedis: finished storing in redis.");
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"storeInRedis: failed to store to host [" +
|
||||
redisHost +
|
||||
"], port [" +
|
||||
redisPort +
|
||||
"]: %o",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export enum Status {
|
||||
Pending = 1,
|
||||
Completed = 2,
|
||||
Error = 3,
|
||||
FatalError = 4,
|
||||
}
|
||||
|
||||
export type RelayResult = {
|
||||
status: Status;
|
||||
result: string | null;
|
||||
};
|
||||
|
||||
export type WorkerInfo = {
|
||||
index: number;
|
||||
targetChainName: string;
|
||||
targetChainId: number;
|
||||
walletPrivateKey: any;
|
||||
};
|
||||
|
||||
export type StoreKey = {
|
||||
chain_id: number;
|
||||
emitter_address: string;
|
||||
sequence: number;
|
||||
};
|
||||
|
||||
export type StorePayload = {
|
||||
vaa_bytes: string;
|
||||
status: Status;
|
||||
timestamp: string;
|
||||
retries: number;
|
||||
};
|
||||
|
||||
/** Default redis payload */
|
||||
export function initPayload(): StorePayload {
|
||||
return {
|
||||
vaa_bytes: "",
|
||||
status: Status.Pending,
|
||||
timestamp: new Date().toISOString(),
|
||||
retries: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function initPayloadWithVAA(vaa_bytes: string): StorePayload {
|
||||
const sp: StorePayload = initPayload();
|
||||
sp.vaa_bytes = vaa_bytes;
|
||||
return sp;
|
||||
}
|
||||
|
||||
export function storeKeyFromParsedVAA(
|
||||
parsedVAA: ParsedVaa<ParsedTransferPayload>
|
||||
): StoreKey {
|
||||
return {
|
||||
chain_id: parsedVAA.emitterChain as number,
|
||||
emitter_address: uint8ArrayToHex(parsedVAA.emitterAddress),
|
||||
sequence: parsedVAA.sequence,
|
||||
};
|
||||
}
|
||||
|
||||
/** Stringify the key going into redis as json */
|
||||
export function storeKeyToJson(storeKey: StoreKey): string {
|
||||
return JSON.stringify(storeKey);
|
||||
}
|
||||
|
||||
export function storeKeyFromJson(json: string): StoreKey {
|
||||
return JSON.parse(json);
|
||||
}
|
||||
|
||||
/** Stringify the value going into redis as json */
|
||||
export function storePayloadToJson(storePayload: StorePayload): string {
|
||||
return JSON.stringify(storePayload);
|
||||
}
|
||||
|
||||
export function storePayloadFromJson(json: string): StorePayload {
|
||||
return JSON.parse(json);
|
||||
}
|
||||
|
||||
export function resetPayload(storePayload: StorePayload): StorePayload {
|
||||
return initPayloadWithVAA(storePayload.vaa_bytes);
|
||||
}
|
||||
|
||||
export async function clearRedis() {
|
||||
const redisClient = await connectToRedis();
|
||||
if (!redisClient) {
|
||||
logger.error("Failed to connect to redis to clear tables.");
|
||||
return;
|
||||
}
|
||||
await redisClient.FLUSHALL();
|
||||
redisClient.quit();
|
||||
}
|
||||
|
||||
export async function demoteWorkingRedis() {
|
||||
const redisClient = await connectToRedis();
|
||||
if (!redisClient) {
|
||||
logger.error("Failed to connect to redis to clear tables.");
|
||||
return;
|
||||
}
|
||||
await redisClient.select(RedisTables.WORKING);
|
||||
for await (const si_key of redisClient.scanIterator()) {
|
||||
const si_value = await redisClient.get(si_key);
|
||||
if (!si_value) {
|
||||
continue;
|
||||
}
|
||||
logger.info("Demoting %s", si_key);
|
||||
await redisClient.del(si_key);
|
||||
await redisClient.select(RedisTables.INCOMING);
|
||||
await redisClient.set(
|
||||
si_key,
|
||||
storePayloadToJson(resetPayload(storePayloadFromJson(si_value)))
|
||||
);
|
||||
await redisClient.select(RedisTables.WORKING);
|
||||
}
|
||||
redisClient.quit();
|
||||
}
|
||||
|
||||
type SourceToTargetMap = {
|
||||
[key in ChainId]: {
|
||||
[key in ChainId]: number;
|
||||
};
|
||||
};
|
||||
|
||||
export function createSourceToTargetMap(
|
||||
knownChainIds: ChainId[]
|
||||
): SourceToTargetMap {
|
||||
const sourceToTargetMap: SourceToTargetMap = {} as SourceToTargetMap;
|
||||
for (const sourceKey of knownChainIds) {
|
||||
sourceToTargetMap[sourceKey] = {} as { [key in ChainId]: number };
|
||||
for (const targetKey of knownChainIds) {
|
||||
sourceToTargetMap[sourceKey][targetKey] = 0;
|
||||
}
|
||||
}
|
||||
return sourceToTargetMap;
|
||||
}
|
||||
|
||||
export async function incrementSourceToTargetMap(
|
||||
key: string,
|
||||
redisClient: RedisClientType<any>,
|
||||
parse_vaa: Function,
|
||||
sourceToTargetMap: SourceToTargetMap
|
||||
): Promise<void> {
|
||||
const parsedKey = storeKeyFromJson(key);
|
||||
const si_value = await redisClient.get(key);
|
||||
if (!si_value) {
|
||||
return;
|
||||
}
|
||||
const parsedPayload = parseTransferPayload(
|
||||
Buffer.from(
|
||||
parse_vaa(hexToUint8Array(storePayloadFromJson(si_value).vaa_bytes))
|
||||
.payload
|
||||
)
|
||||
);
|
||||
if (
|
||||
sourceToTargetMap[parsedKey.chain_id as ChainId]?.[
|
||||
parsedPayload.targetChain
|
||||
] !== undefined
|
||||
) {
|
||||
sourceToTargetMap[parsedKey.chain_id as ChainId][
|
||||
parsedPayload.targetChain
|
||||
]++;
|
||||
}
|
||||
}
|
||||
|
||||
export async function monitorRedis(metrics: PromHelper) {
|
||||
const scopedLogger = getScopedLogger(["monitorRedis"], logger);
|
||||
const TEN_SECONDS: number = 10000;
|
||||
const { parse_vaa } = await importCoreWasm();
|
||||
const knownChainIds = Object.keys(chainIDStrings).map(
|
||||
(c) => Number(c) as ChainId
|
||||
);
|
||||
while (true) {
|
||||
const redisClient = await connectToRedis();
|
||||
if (!redisClient) {
|
||||
scopedLogger.error("Failed to connect to redis!");
|
||||
} else {
|
||||
try {
|
||||
await redisClient.select(RedisTables.INCOMING);
|
||||
const incomingSourceToTargetMap =
|
||||
createSourceToTargetMap(knownChainIds);
|
||||
for await (const si_key of redisClient.scanIterator()) {
|
||||
incrementSourceToTargetMap(
|
||||
si_key,
|
||||
redisClient,
|
||||
parse_vaa,
|
||||
incomingSourceToTargetMap
|
||||
);
|
||||
}
|
||||
for (const sourceKey of knownChainIds) {
|
||||
for (const targetKey of knownChainIds) {
|
||||
metrics.setRedisQueue(
|
||||
RedisTables.INCOMING,
|
||||
sourceKey,
|
||||
targetKey,
|
||||
incomingSourceToTargetMap[sourceKey][targetKey]
|
||||
);
|
||||
}
|
||||
}
|
||||
await redisClient.select(RedisTables.WORKING);
|
||||
const workingSourceToTargetMap = createSourceToTargetMap(knownChainIds);
|
||||
for await (const si_key of redisClient.scanIterator()) {
|
||||
incrementSourceToTargetMap(
|
||||
si_key,
|
||||
redisClient,
|
||||
parse_vaa,
|
||||
workingSourceToTargetMap
|
||||
);
|
||||
}
|
||||
for (const sourceKey of knownChainIds) {
|
||||
for (const targetKey of knownChainIds) {
|
||||
metrics.setRedisQueue(
|
||||
RedisTables.WORKING,
|
||||
sourceKey,
|
||||
targetKey,
|
||||
workingSourceToTargetMap[sourceKey][targetKey]
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
scopedLogger.error("Failed to get dbSize and set metrics!");
|
||||
}
|
||||
try {
|
||||
redisClient.quit();
|
||||
} catch (e) {}
|
||||
}
|
||||
await sleep(TEN_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check to see if a key is in the listener memory queue or redis incoming db */
|
||||
export async function checkQueue(key: string): Promise<string | null> {
|
||||
try {
|
||||
const backupQueue = getBackupQueue();
|
||||
const queuedRecord = backupQueue.find((record) => record[0] === key);
|
||||
|
||||
if (queuedRecord) {
|
||||
logger.debug("VAA was already in the listener queue");
|
||||
return "VAA was already in the listener queue";
|
||||
}
|
||||
|
||||
const rClient = await connectToRedis();
|
||||
if (!rClient) {
|
||||
logger.error("Failed to connect to redis");
|
||||
return null;
|
||||
}
|
||||
|
||||
await rClient.select(RedisTables.INCOMING);
|
||||
const record1 = await rClient.get(key);
|
||||
|
||||
if (record1) {
|
||||
logger.debug("VAA was already in INCOMING table");
|
||||
rClient.quit();
|
||||
return "VAA was already in INCOMING table";
|
||||
}
|
||||
|
||||
await rClient.select(RedisTables.WORKING);
|
||||
const record2 = await rClient.get(key);
|
||||
if (record2) {
|
||||
logger.debug("VAA was already in WORKING table");
|
||||
rClient.quit();
|
||||
return "VAA was already in WORKING table";
|
||||
}
|
||||
rClient.quit();
|
||||
} catch (e) {
|
||||
logger.error("Failed to connect to redis");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -1,463 +0,0 @@
|
|||
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js";
|
||||
import * as BN from "bn.js";
|
||||
import { deserializeUnchecked } from "borsh";
|
||||
import { BinaryReader, BinaryWriter } from "borsh";
|
||||
import { ChainConfigInfo } from "../configureEnv";
|
||||
import { getMultipleAccountsRPC } from "../utils/solana";
|
||||
const base58: any = require("bs58");
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const METADATA_REPLACE = new RegExp("\u0000", "g");
|
||||
export const EDITION_MARKER_BIT_SIZE = 248;
|
||||
export const METADATA_PREFIX = "metadata";
|
||||
export const EDITION = "edition";
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export type StringPublicKey = string;
|
||||
|
||||
export enum MetadataKey {
|
||||
Uninitialized = 0,
|
||||
MetadataV1 = 4,
|
||||
EditionV1 = 1,
|
||||
MasterEditionV1 = 2,
|
||||
MasterEditionV2 = 6,
|
||||
EditionMarker = 7,
|
||||
}
|
||||
|
||||
export const METADATA_PROGRAM_ID =
|
||||
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" as StringPublicKey;
|
||||
|
||||
export class Data {
|
||||
name: string;
|
||||
symbol: string;
|
||||
uri: string;
|
||||
// sellerFeeBasisPoints: number;
|
||||
// creators: Creator[] | null;
|
||||
constructor(args: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
uri: string;
|
||||
// sellerFeeBasisPoints: number;
|
||||
// creators: Creator[] | null;
|
||||
}) {
|
||||
this.name = args.name;
|
||||
this.symbol = args.symbol;
|
||||
this.uri = args.uri;
|
||||
// this.sellerFeeBasisPoints = args.sellerFeeBasisPoints;
|
||||
// this.creators = args.creators;
|
||||
}
|
||||
}
|
||||
|
||||
class CreateMetadataArgs {
|
||||
instruction: number = 0;
|
||||
data: Data;
|
||||
isMutable: boolean;
|
||||
|
||||
constructor(args: { data: Data; isMutable: boolean }) {
|
||||
this.data = args.data;
|
||||
this.isMutable = args.isMutable;
|
||||
}
|
||||
}
|
||||
class UpdateMetadataArgs {
|
||||
instruction: number = 1;
|
||||
data: Data | null;
|
||||
// Not used by this app, just required for instruction
|
||||
updateAuthority: StringPublicKey | null;
|
||||
primarySaleHappened: boolean | null;
|
||||
constructor(args: {
|
||||
data?: Data;
|
||||
updateAuthority?: string;
|
||||
primarySaleHappened: boolean | null;
|
||||
}) {
|
||||
this.data = args.data ? args.data : null;
|
||||
this.updateAuthority = args.updateAuthority ? args.updateAuthority : null;
|
||||
this.primarySaleHappened = args.primarySaleHappened;
|
||||
}
|
||||
}
|
||||
|
||||
class CreateMasterEditionArgs {
|
||||
instruction: number = 10;
|
||||
maxSupply: BN | null;
|
||||
constructor(args: { maxSupply: BN | null }) {
|
||||
this.maxSupply = args.maxSupply;
|
||||
}
|
||||
}
|
||||
|
||||
class MintPrintingTokensArgs {
|
||||
instruction: number = 9;
|
||||
supply: BN;
|
||||
|
||||
constructor(args: { supply: BN }) {
|
||||
this.supply = args.supply;
|
||||
}
|
||||
}
|
||||
|
||||
export class MasterEditionV1 {
|
||||
key: MetadataKey;
|
||||
supply: BN;
|
||||
maxSupply?: BN;
|
||||
/// Can be used to mint tokens that give one-time permission to mint a single limited edition.
|
||||
printingMint: StringPublicKey;
|
||||
/// If you don't know how many printing tokens you are going to need, but you do know
|
||||
/// you are going to need some amount in the future, you can use a token from this mint.
|
||||
/// Coming back to token metadata with one of these tokens allows you to mint (one time)
|
||||
/// any number of printing tokens you want. This is used for instance by Auction Manager
|
||||
/// with participation NFTs, where we dont know how many people will bid and need participation
|
||||
/// printing tokens to redeem, so we give it ONE of these tokens to use after the auction is over,
|
||||
/// because when the auction begins we just dont know how many printing tokens we will need,
|
||||
/// but at the end we will. At the end it then burns this token with token-metadata to
|
||||
/// get the printing tokens it needs to give to bidders. Each bidder then redeems a printing token
|
||||
/// to get their limited editions.
|
||||
oneTimePrintingAuthorizationMint: StringPublicKey;
|
||||
|
||||
constructor(args: {
|
||||
key: MetadataKey;
|
||||
supply: BN;
|
||||
maxSupply?: BN;
|
||||
printingMint: StringPublicKey;
|
||||
oneTimePrintingAuthorizationMint: StringPublicKey;
|
||||
}) {
|
||||
this.key = MetadataKey.MasterEditionV1;
|
||||
this.supply = args.supply;
|
||||
this.maxSupply = args.maxSupply;
|
||||
this.printingMint = args.printingMint;
|
||||
this.oneTimePrintingAuthorizationMint =
|
||||
args.oneTimePrintingAuthorizationMint;
|
||||
}
|
||||
}
|
||||
|
||||
export class MasterEditionV2 {
|
||||
key: MetadataKey;
|
||||
supply: BN;
|
||||
maxSupply?: BN;
|
||||
|
||||
constructor(args: { key: MetadataKey; supply: BN; maxSupply?: BN }) {
|
||||
this.key = MetadataKey.MasterEditionV2;
|
||||
this.supply = args.supply;
|
||||
this.maxSupply = args.maxSupply;
|
||||
}
|
||||
}
|
||||
|
||||
export class Edition {
|
||||
key: MetadataKey;
|
||||
/// Points at MasterEdition struct
|
||||
parent: StringPublicKey;
|
||||
/// Starting at 0 for master record, this is incremented for each edition minted.
|
||||
edition: BN;
|
||||
|
||||
constructor(args: {
|
||||
key: MetadataKey;
|
||||
parent: StringPublicKey;
|
||||
edition: BN;
|
||||
}) {
|
||||
this.key = MetadataKey.EditionV1;
|
||||
this.parent = args.parent;
|
||||
this.edition = args.edition;
|
||||
}
|
||||
}
|
||||
|
||||
export class Creator {
|
||||
address: StringPublicKey;
|
||||
verified: boolean;
|
||||
share: number;
|
||||
|
||||
constructor(args: {
|
||||
address: StringPublicKey;
|
||||
verified: boolean;
|
||||
share: number;
|
||||
}) {
|
||||
this.address = args.address;
|
||||
this.verified = args.verified;
|
||||
this.share = args.share;
|
||||
}
|
||||
}
|
||||
|
||||
export class Metadata {
|
||||
key: MetadataKey;
|
||||
updateAuthority: StringPublicKey;
|
||||
mint: StringPublicKey;
|
||||
data: Data;
|
||||
primarySaleHappened: boolean;
|
||||
isMutable: boolean;
|
||||
editionNonce: number | null;
|
||||
|
||||
// set lazy
|
||||
masterEdition?: StringPublicKey;
|
||||
edition?: StringPublicKey;
|
||||
|
||||
constructor(args: {
|
||||
updateAuthority: StringPublicKey;
|
||||
mint: StringPublicKey;
|
||||
data: Data;
|
||||
primarySaleHappened: boolean;
|
||||
isMutable: boolean;
|
||||
editionNonce: number | null;
|
||||
}) {
|
||||
this.key = MetadataKey.MetadataV1;
|
||||
this.updateAuthority = args.updateAuthority;
|
||||
this.mint = args.mint;
|
||||
this.data = args.data;
|
||||
this.primarySaleHappened = args.primarySaleHappened;
|
||||
this.isMutable = args.isMutable;
|
||||
this.editionNonce = args.editionNonce;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
// const edition = await getEdition(this.mint);
|
||||
const edition = "0";
|
||||
this.edition = edition;
|
||||
this.masterEdition = edition;
|
||||
}
|
||||
}
|
||||
|
||||
export class EditionMarker {
|
||||
key: MetadataKey;
|
||||
ledger: number[];
|
||||
|
||||
constructor(args: { key: MetadataKey; ledger: number[] }) {
|
||||
this.key = MetadataKey.EditionMarker;
|
||||
this.ledger = args.ledger;
|
||||
}
|
||||
|
||||
editionTaken(edition: number) {
|
||||
const editionOffset = edition % EDITION_MARKER_BIT_SIZE;
|
||||
const indexOffset = Math.floor(editionOffset / 8);
|
||||
|
||||
if (indexOffset > 30) {
|
||||
throw new Error("bad index for edition");
|
||||
}
|
||||
|
||||
const positionInBitsetFromRight = 7 - (editionOffset % 8);
|
||||
|
||||
const mask = Math.pow(2, positionInBitsetFromRight);
|
||||
|
||||
const appliedMask = this.ledger[indexOffset] & mask;
|
||||
|
||||
// eslint-disable-next-line
|
||||
return appliedMask != 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const METADATA_SCHEMA = new Map<any, any>([
|
||||
[
|
||||
CreateMetadataArgs,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["instruction", "u8"],
|
||||
["data", Data],
|
||||
["isMutable", "u8"], // bool
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
UpdateMetadataArgs,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["instruction", "u8"],
|
||||
["data", { kind: "option", type: Data }],
|
||||
["updateAuthority", { kind: "option", type: "pubkeyAsString" }],
|
||||
["primarySaleHappened", { kind: "option", type: "u8" }],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
CreateMasterEditionArgs,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["instruction", "u8"],
|
||||
["maxSupply", { kind: "option", type: "u64" }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
MintPrintingTokensArgs,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["instruction", "u8"],
|
||||
["supply", "u64"],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
MasterEditionV1,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["key", "u8"],
|
||||
["supply", "u64"],
|
||||
["maxSupply", { kind: "option", type: "u64" }],
|
||||
["printingMint", "pubkeyAsString"],
|
||||
["oneTimePrintingAuthorizationMint", "pubkeyAsString"],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
MasterEditionV2,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["key", "u8"],
|
||||
["supply", "u64"],
|
||||
["maxSupply", { kind: "option", type: "u64" }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
Edition,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["key", "u8"],
|
||||
["parent", "pubkeyAsString"],
|
||||
["edition", "u64"],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
Data,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["name", "string"],
|
||||
["symbol", "string"],
|
||||
["uri", "string"],
|
||||
["sellerFeeBasisPoints", "u16"],
|
||||
["creators", { kind: "option", type: [Creator] }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
Creator,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["address", "pubkeyAsString"],
|
||||
["verified", "u8"],
|
||||
["share", "u8"],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
Metadata,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["key", "u8"],
|
||||
["updateAuthority", "pubkeyAsString"],
|
||||
["mint", "pubkeyAsString"],
|
||||
["data", Data],
|
||||
["primarySaleHappened", "u8"], // bool
|
||||
["isMutable", "u8"], // bool
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
EditionMarker,
|
||||
{
|
||||
kind: "struct",
|
||||
fields: [
|
||||
["key", "u8"],
|
||||
["ledger", [31]],
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
export const extendBorsh = () => {
|
||||
(BinaryReader.prototype as any).readPubkey = function () {
|
||||
const reader = this as unknown as BinaryReader;
|
||||
const array = reader.readFixedArray(32);
|
||||
return new PublicKey(array);
|
||||
};
|
||||
|
||||
(BinaryWriter.prototype as any).writePubkey = function (value: PublicKey) {
|
||||
const writer = this as unknown as BinaryWriter;
|
||||
writer.writeFixedArray(value.toBuffer());
|
||||
};
|
||||
|
||||
(BinaryReader.prototype as any).readPubkeyAsString = function () {
|
||||
const reader = this as unknown as BinaryReader;
|
||||
const array = reader.readFixedArray(32);
|
||||
return base58.encode(array) as StringPublicKey;
|
||||
};
|
||||
|
||||
(BinaryWriter.prototype as any).writePubkeyAsString = function (
|
||||
value: StringPublicKey
|
||||
) {
|
||||
const writer = this as unknown as BinaryWriter;
|
||||
writer.writeFixedArray(base58.decode(value));
|
||||
};
|
||||
};
|
||||
|
||||
extendBorsh();
|
||||
|
||||
export const decodeMetadata = (buffer: Buffer): Metadata => {
|
||||
const metadata = deserializeUnchecked(
|
||||
METADATA_SCHEMA,
|
||||
Metadata,
|
||||
buffer
|
||||
) as Metadata;
|
||||
metadata.data.name = metadata.data.name.replace(METADATA_REPLACE, "");
|
||||
metadata.data.uri = metadata.data.uri.replace(METADATA_REPLACE, "");
|
||||
metadata.data.symbol = metadata.data.symbol.replace(METADATA_REPLACE, "");
|
||||
return metadata;
|
||||
};
|
||||
|
||||
export const getMetadataAddress = async (
|
||||
mintKey: string
|
||||
): Promise<[PublicKey, number]> => {
|
||||
const seeds = [
|
||||
Buffer.from("metadata"),
|
||||
new PublicKey(METADATA_PROGRAM_ID).toBuffer(),
|
||||
new PublicKey(mintKey).toBuffer(),
|
||||
];
|
||||
return PublicKey.findProgramAddress(
|
||||
seeds,
|
||||
new PublicKey(METADATA_PROGRAM_ID)
|
||||
);
|
||||
};
|
||||
|
||||
export const getMetaplexData = async (
|
||||
mintAddresses: string[],
|
||||
chainInfo: ChainConfigInfo
|
||||
) => {
|
||||
const promises = [];
|
||||
for (const address of mintAddresses) {
|
||||
promises.push(getMetadataAddress(address));
|
||||
}
|
||||
const metaAddresses = await Promise.all(promises);
|
||||
// const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const connection = new Connection(chainInfo.nodeUrl, "confirmed");
|
||||
const results = await getMultipleAccountsRPC(
|
||||
connection,
|
||||
metaAddresses.map((pair) => pair && pair[0])
|
||||
);
|
||||
|
||||
const output = results.map((account) => {
|
||||
if (account === null) {
|
||||
return undefined;
|
||||
} else {
|
||||
if (account.data) {
|
||||
try {
|
||||
const MetadataParsed = decodeMetadata(account.data);
|
||||
return MetadataParsed;
|
||||
} catch (e) {
|
||||
// console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
import { Request, Response } from "express";
|
||||
import { getBackend } from "../backends";
|
||||
import { getListenerEnvironment, ListenerEnvironment } from "../configureEnv";
|
||||
import { getLogger } from "../helpers/logHelper";
|
||||
|
||||
let logger = getLogger();
|
||||
let env: ListenerEnvironment;
|
||||
|
||||
export function init(): boolean {
|
||||
try {
|
||||
env = getListenerEnvironment();
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Encountered and error while initializing the listener environment: " + e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!env.restPort) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
if (!env.restPort) return;
|
||||
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.listen(env.restPort, () =>
|
||||
logger.info("listening on REST port %d!", env.restPort)
|
||||
);
|
||||
|
||||
(async () => {
|
||||
app.get("/relayvaa/:vaa", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const rawVaa = Uint8Array.from(Buffer.from(req.params.vaa, "base64"));
|
||||
await getBackend().listener.process(rawVaa);
|
||||
|
||||
res.status(200).json({ message: "Scheduled" });
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"failed to process rest relay of vaa request, error: %o",
|
||||
e
|
||||
);
|
||||
logger.error("offending request: %o", req);
|
||||
res.status(400).json({ message: "Request failed" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/", (req: Request, res: Response) =>
|
||||
res.json(["/relayvaa/<vaaInBase64>"])
|
||||
);
|
||||
})();
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
import {
|
||||
createSpyRPCServiceClient,
|
||||
subscribeSignedVAA,
|
||||
} from "@certusone/wormhole-spydk";
|
||||
import { getBackend } from "../backends";
|
||||
import { getListenerEnvironment, ListenerEnvironment } from "../configureEnv";
|
||||
import { getLogger } from "../helpers/logHelper";
|
||||
import { PromHelper } from "../helpers/promHelpers";
|
||||
import { sleep } from "../helpers/utils";
|
||||
|
||||
let metrics: PromHelper;
|
||||
let env: ListenerEnvironment;
|
||||
let logger = getLogger();
|
||||
let vaaUriPrelude: string;
|
||||
|
||||
export function init(): boolean {
|
||||
try {
|
||||
env = getListenerEnvironment();
|
||||
vaaUriPrelude =
|
||||
"http://localhost:" +
|
||||
(process.env.REST_PORT ? process.env.REST_PORT : "4201") +
|
||||
"/relayvaa/";
|
||||
} catch (e) {
|
||||
logger.error("Error initializing listener environment: " + e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function run(ph: PromHelper) {
|
||||
const logger = getLogger();
|
||||
metrics = ph;
|
||||
logger.info("Attempting to run Listener...");
|
||||
logger.info(
|
||||
"spy_relay starting up, will listen for signed VAAs from [" +
|
||||
env.spyServiceHost +
|
||||
"]"
|
||||
);
|
||||
|
||||
let typedFilters = await getBackend().listener.getEmitterFilters();
|
||||
const wrappedFilters = { filters: typedFilters };
|
||||
|
||||
while (true) {
|
||||
let stream: any;
|
||||
try {
|
||||
const client = createSpyRPCServiceClient(env.spyServiceHost || "");
|
||||
stream = await subscribeSignedVAA(client, wrappedFilters);
|
||||
|
||||
//TODO validate that this is the correct type of the vaaBytes
|
||||
stream.on("data", ({ vaaBytes }: { vaaBytes: Buffer }) => {
|
||||
metrics.incIncoming();
|
||||
const asUint8 = new Uint8Array(vaaBytes);
|
||||
getBackend().listener.process(asUint8);
|
||||
});
|
||||
|
||||
let connected = true;
|
||||
stream.on("error", (err: any) => {
|
||||
logger.error("spy service returned an error: %o", err);
|
||||
connected = false;
|
||||
});
|
||||
|
||||
stream.on("close", () => {
|
||||
logger.error("spy service closed the connection!");
|
||||
connected = false;
|
||||
});
|
||||
|
||||
logger.info(
|
||||
"connected to spy service, listening for transfer signed VAAs"
|
||||
);
|
||||
|
||||
while (connected) {
|
||||
await sleep(1000);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("spy service threw an exception: %o", e);
|
||||
}
|
||||
|
||||
stream.destroy()
|
||||
await sleep(5 * 1000);
|
||||
logger.info("attempting to reconnect to the spy service");
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import { ChainId } from "@certusone/wormhole-sdk";
|
||||
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
|
||||
|
||||
//TODO move these to the official SDK
|
||||
export async function parseVaaTyped(signedVAA: Uint8Array) {
|
||||
const { parse_vaa } = await importCoreWasm();
|
||||
const parsedVAA = parse_vaa(signedVAA);
|
||||
return {
|
||||
timestamp: parseInt(parsedVAA.timestamp),
|
||||
nonce: parseInt(parsedVAA.nonce),
|
||||
emitterChain: parseInt(parsedVAA.emitter_chain) as ChainId,
|
||||
emitterAddress: parsedVAA.emitter_address, //This will be in wormhole HEX format
|
||||
sequence: parseInt(parsedVAA.sequence),
|
||||
consistencyLevel: parseInt(parsedVAA.consistency_level),
|
||||
payload: parsedVAA.payload,
|
||||
};
|
||||
}
|
||||
|
||||
export type ParsedVaa<T> = {
|
||||
timestamp: number;
|
||||
nonce: number;
|
||||
emitterChain: ChainId;
|
||||
emitterAddress: Uint8Array;
|
||||
sequence: number;
|
||||
consistencyLevel: number;
|
||||
payload: T;
|
||||
};
|
||||
|
||||
export type ParsedTransferPayload = {
|
||||
amount: BigInt;
|
||||
originAddress: string; // hex
|
||||
originChain: ChainId;
|
||||
targetAddress: string; // hex
|
||||
targetChain: ChainId;
|
||||
fee?: BigInt;
|
||||
};
|
||||
|
||||
/** Type guard function to ensure an object is of type ParsedTransferPayload */
|
||||
function IsParsedTransferPayload(
|
||||
payload: any
|
||||
): payload is ParsedTransferPayload {
|
||||
return (
|
||||
typeof (payload as ParsedTransferPayload).amount == "bigint" &&
|
||||
typeof (payload as ParsedTransferPayload).originAddress == "string" &&
|
||||
typeof (payload as ParsedTransferPayload).originChain == "number" &&
|
||||
typeof (payload as ParsedTransferPayload).targetAddress == "string" &&
|
||||
typeof (payload as ParsedTransferPayload).targetChain == "number"
|
||||
);
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
//This has to run first so that the process variables are set up when the other modules are instantiated.
|
||||
require("./helpers/loadConfig");
|
||||
|
||||
import { setDefaultWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
|
||||
import { getCommonEnvironment } from "./configureEnv";
|
||||
import { getLogger } from "./helpers/logHelper";
|
||||
import { PromHelper, PromMode } from "./helpers/promHelpers";
|
||||
import * as redisHelper from "./helpers/redisHelper";
|
||||
import * as restListener from "./listener/rest_listen";
|
||||
import * as spyListener from "./listener/spy_listen";
|
||||
import * as relayWorker from "./relayer/relay_worker";
|
||||
import * as walletMonitor from "./monitor";
|
||||
|
||||
const ARG_LISTEN_ONLY = "--listen_only";
|
||||
const ARG_RELAY_ONLY = "--relay_only";
|
||||
const ARG_WALLET_MONITOR_ONLY = "--wallet_monitor_only";
|
||||
const ONLY_ONE_ARG_ERROR_MSG = `May only specify one of ${ARG_LISTEN_ONLY}, ${ARG_RELAY_ONLY}, or ${ARG_WALLET_MONITOR_ONLY}`;
|
||||
const ONLY_ONE_ARG_ERROR_RESULT = `Multiple args found of ${ARG_LISTEN_ONLY}, ${ARG_RELAY_ONLY}, ${ARG_WALLET_MONITOR_ONLY}`;
|
||||
|
||||
setDefaultWasm("node");
|
||||
const logger = getLogger();
|
||||
|
||||
// Load the relay config data.
|
||||
let runListen: boolean = true;
|
||||
let runRelayWorker: boolean = true;
|
||||
let runRest: boolean = true;
|
||||
let runWalletMonitor: boolean = true;
|
||||
let foundOne: boolean = false;
|
||||
let error: string = "";
|
||||
|
||||
for (let idx = 0; idx < process.argv.length; ++idx) {
|
||||
if (process.argv[idx] === ARG_LISTEN_ONLY) {
|
||||
if (foundOne) {
|
||||
logger.error(ONLY_ONE_ARG_ERROR_MSG);
|
||||
error = ONLY_ONE_ARG_ERROR_RESULT;
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info("spy_relay is running in listen only mode");
|
||||
runRelayWorker = false;
|
||||
runWalletMonitor = false;
|
||||
foundOne = true;
|
||||
}
|
||||
|
||||
if (process.argv[idx] === ARG_RELAY_ONLY) {
|
||||
if (foundOne) {
|
||||
logger.error(ONLY_ONE_ARG_ERROR_MSG);
|
||||
error = ONLY_ONE_ARG_ERROR_RESULT;
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info("spy_relay is running in relay only mode");
|
||||
runListen = false;
|
||||
runRest = false;
|
||||
runWalletMonitor = false;
|
||||
foundOne = true;
|
||||
}
|
||||
|
||||
if (process.argv[idx] === ARG_WALLET_MONITOR_ONLY) {
|
||||
if (foundOne) {
|
||||
logger.error(ONLY_ONE_ARG_ERROR_MSG);
|
||||
error = ONLY_ONE_ARG_ERROR_RESULT;
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info("spy_relay is running in wallet monitor only mode");
|
||||
runListen = false;
|
||||
runRest = false;
|
||||
runRelayWorker = false;
|
||||
foundOne = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundOne) {
|
||||
logger.info("spy_relay is running both the listener and relayer");
|
||||
}
|
||||
|
||||
const runAll: boolean = runListen && runRelayWorker && runWalletMonitor;
|
||||
if (runListen && !spyListener.init()) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (runRelayWorker && !relayWorker.init()) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (runRest && !restListener.init()) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (runWalletMonitor && !walletMonitor.init()) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
logger.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const commonEnv = getCommonEnvironment();
|
||||
const { promPort, readinessPort } = commonEnv;
|
||||
logger.info("prometheus client listening on port " + promPort);
|
||||
let promClient: PromHelper;
|
||||
if (runAll) {
|
||||
promClient = new PromHelper("spy_relay", promPort, PromMode.All);
|
||||
} else if (runListen) {
|
||||
promClient = new PromHelper("spy_relay", promPort, PromMode.Listen);
|
||||
} else if (runRelayWorker) {
|
||||
promClient = new PromHelper("spy_relay", promPort, PromMode.Relay);
|
||||
} else if (runWalletMonitor) {
|
||||
promClient = new PromHelper("spy_relay", promPort, PromMode.WalletMonitor);
|
||||
} else {
|
||||
logger.error("Invalid run mode for Prometheus");
|
||||
promClient = new PromHelper("spy_relay", promPort, PromMode.All);
|
||||
}
|
||||
|
||||
redisHelper.init(promClient);
|
||||
|
||||
if (runListen) spyListener.run(promClient);
|
||||
if (runRelayWorker) relayWorker.run(promClient);
|
||||
if (runRest) restListener.run();
|
||||
if (runWalletMonitor) walletMonitor.run(promClient);
|
||||
|
||||
if (readinessPort) {
|
||||
const Net = require("net");
|
||||
const readinessServer = new Net.Server();
|
||||
readinessServer.listen(readinessPort, function () {
|
||||
logger.info("listening for readiness requests on port " + readinessPort);
|
||||
});
|
||||
|
||||
readinessServer.on("connection", function (socket: any) {
|
||||
//logger.debug("readiness connection");
|
||||
});
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { getRelayerEnvironment, RelayerEnvironment } from "../configureEnv";
|
||||
import { getLogger } from "../helpers/logHelper";
|
||||
import { PromHelper } from "../helpers/promHelpers";
|
||||
import { collectWallets } from "./walletMonitor";
|
||||
|
||||
let metrics: PromHelper;
|
||||
|
||||
const logger = getLogger();
|
||||
let relayerEnv: RelayerEnvironment;
|
||||
|
||||
export function init(): boolean {
|
||||
try {
|
||||
relayerEnv = getRelayerEnvironment();
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Encountered error while initiating the monitor environment: " + e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function run(ph: PromHelper) {
|
||||
metrics = ph;
|
||||
|
||||
try {
|
||||
collectWallets(metrics);
|
||||
} catch (e) {
|
||||
logger.error("Failed to kick off collectWallets: " + e);
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
require("../helpers/loadConfig");
|
||||
process.env.LOG_DIR = ".";
|
||||
|
||||
import { CHAIN_ID_TERRA, CHAIN_ID_TERRA2 } from "@certusone/wormhole-sdk";
|
||||
import { jest, test } from "@jest/globals";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { ChainConfigInfo } from "../configureEnv";
|
||||
import { calcLocalAddressesTerra, pullTerraBalance } from "./walletMonitor";
|
||||
// import { pullEVMBalance } from "./walletMonitor";
|
||||
|
||||
jest.setTimeout(300000);
|
||||
|
||||
// const bscChainConfig: ChainConfigInfo = {
|
||||
// chainId: CHAIN_ID_BSC,
|
||||
// chainName: "BSC",
|
||||
// nativeCurrencySymbol: "BNB",
|
||||
// nodeUrl: "https://bsc-dataseed.binance.org",
|
||||
// tokenBridgeAddress: "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7",
|
||||
// wrappedAsset: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
||||
// };
|
||||
// const bscPublicKey = "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7"; // Token Bridge
|
||||
// const bscTokens = [
|
||||
// "0xfA54fF1a158B5189Ebba6ae130CEd6bbd3aEA76e", // SOL
|
||||
// "0x4DB5a66E937A9F4473fA95b1cAF1d1E1D62E29EA", // WETH
|
||||
// "0x156ab3346823B651294766e23e6Cf87254d68962", // LUNA
|
||||
// "0x3d4350cD54aeF9f9b2C29435e0fa809957B3F30a", // UST
|
||||
// "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", // WBNB
|
||||
// "0xc836d8dC361E44DbE64c4862D55BA041F88Ddd39", // WMATIC
|
||||
// "0x96412902aa9aFf61E13f085e70D3152C6ef2a817", // WAVAX
|
||||
// "0x6c6D604D3f07aBE287C1A3dF0281e999A83495C0", // wROSE
|
||||
// "0xbF8413EE8612E0E4f66Aa63B5ebE27f3C5883d47", // WFTM
|
||||
// "0xB04906e95AB5D797aDA81508115611fee694c2b3", // USDC
|
||||
// "0x524bC91Dc82d6b90EF29F76A3ECAaBAffFD490Bc", // USDT
|
||||
// ];
|
||||
|
||||
// test("should pull EVM token balances", async () => {
|
||||
// for (let address of bscTokens) {
|
||||
// const balance = await pullEVMBalance(bscChainConfig, bscPublicKey, address);
|
||||
// console.log(balance);
|
||||
// expect(balance).toBeTruthy();
|
||||
// }
|
||||
// });
|
||||
|
||||
const terraClassicChainConfig: ChainConfigInfo = {
|
||||
chainId: CHAIN_ID_TERRA,
|
||||
chainName: "Terra Classic",
|
||||
nativeCurrencySymbol: "LUNC",
|
||||
nodeUrl: "https://columbus-fcd.terra.dev",
|
||||
tokenBridgeAddress: "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf",
|
||||
terraName: "mainnet",
|
||||
terraChainId: "columbus-5",
|
||||
terraCoin: "uluna",
|
||||
terraGasPriceUrl: "https://columbus-fcd.terra.dev/v1/txs/gas_prices",
|
||||
};
|
||||
|
||||
const supportedTokens = require("../../config/mainnet/supportedTokens.json");
|
||||
|
||||
test("should pull Terra Classic token balances", async () => {
|
||||
if (
|
||||
!(
|
||||
terraClassicChainConfig.terraChainId &&
|
||||
terraClassicChainConfig.terraCoin &&
|
||||
terraClassicChainConfig.terraGasPriceUrl &&
|
||||
terraClassicChainConfig.terraName
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"Terra Classic relay was called without proper instantiation."
|
||||
);
|
||||
}
|
||||
const lcdConfig = {
|
||||
URL: terraClassicChainConfig.nodeUrl,
|
||||
chainID: terraClassicChainConfig.terraChainId,
|
||||
name: terraClassicChainConfig.terraName,
|
||||
isClassic: true,
|
||||
};
|
||||
const lcd = new LCDClient(lcdConfig);
|
||||
const localAddresses = await calcLocalAddressesTerra(
|
||||
lcd,
|
||||
supportedTokens,
|
||||
terraClassicChainConfig
|
||||
);
|
||||
expect(localAddresses.length).toBeGreaterThan(0);
|
||||
for (const tokenAddress of localAddresses) {
|
||||
const balance = await pullTerraBalance(
|
||||
lcd,
|
||||
terraClassicChainConfig.tokenBridgeAddress,
|
||||
tokenAddress,
|
||||
CHAIN_ID_TERRA
|
||||
);
|
||||
console.log(balance);
|
||||
expect(balance).toBeDefined();
|
||||
}
|
||||
});
|
|
@ -1,536 +0,0 @@
|
|||
import {
|
||||
Bridge__factory,
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
getForeignAssetTerra,
|
||||
hexToUint8Array,
|
||||
isEVMChain,
|
||||
isTerraChain,
|
||||
nativeToHexString,
|
||||
TerraChainId,
|
||||
WSOL_DECIMALS,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import { Connection, Keypair } from "@solana/web3.js";
|
||||
import { LCDClient, MnemonicKey } from "@terra-money/terra.js";
|
||||
import { ethers, Signer } from "ethers";
|
||||
import { formatUnits } from "ethers/lib/utils";
|
||||
import {
|
||||
ChainConfigInfo,
|
||||
getRelayerEnvironment,
|
||||
RelayerEnvironment,
|
||||
SupportedToken,
|
||||
} from "../configureEnv";
|
||||
import { getScopedLogger } from "../helpers/logHelper";
|
||||
import { PromHelper } from "../helpers/promHelpers";
|
||||
import { getMetaplexData, sleep } from "../helpers/utils";
|
||||
import { getEthereumToken } from "../utils/ethereum";
|
||||
import { getMultipleAccountsRPC } from "../utils/solana";
|
||||
import { formatNativeDenom } from "../utils/terra";
|
||||
import { newProvider } from "../relayer/evm";
|
||||
|
||||
let env: RelayerEnvironment;
|
||||
const logger = getScopedLogger(["walletMonitor"]);
|
||||
|
||||
export type WalletBalance = {
|
||||
chainId: ChainId;
|
||||
balanceAbs: string;
|
||||
balanceFormatted?: string;
|
||||
currencyName: string;
|
||||
currencyAddressNative: string;
|
||||
isNative: boolean;
|
||||
walletAddress: string;
|
||||
};
|
||||
|
||||
export interface TerraNativeBalances {
|
||||
[index: string]: string;
|
||||
}
|
||||
|
||||
async function pullBalances(metrics: PromHelper): Promise<WalletBalance[]> {
|
||||
env = getRelayerEnvironment();
|
||||
//TODO loop through all the chain configs, calc the public keys, pull their balances, and push to a combo of the loggers and prmometheus
|
||||
if (!env) {
|
||||
logger.error("pullBalances() - no env");
|
||||
return [];
|
||||
}
|
||||
if (!env.supportedChains) {
|
||||
logger.error("pullBalances() - no supportedChains");
|
||||
return [];
|
||||
}
|
||||
const balancePromises: Promise<WalletBalance[]>[] = [];
|
||||
for (const chainInfo of env.supportedChains) {
|
||||
if (!chainInfo) continue;
|
||||
try {
|
||||
if (chainInfo.chainId === CHAIN_ID_SOLANA) {
|
||||
for (const solanaPrivateKey of chainInfo.solanaPrivateKey || []) {
|
||||
try {
|
||||
balancePromises.push(
|
||||
pullSolanaNativeBalance(chainInfo, solanaPrivateKey)
|
||||
);
|
||||
balancePromises.push(
|
||||
pullSolanaTokenBalances(chainInfo, solanaPrivateKey)
|
||||
);
|
||||
} catch (e: any) {
|
||||
logger.error(
|
||||
"pulling balances failed failed for chain: " + chainInfo.chainName
|
||||
);
|
||||
if (e && e.stack) {
|
||||
logger.error(e.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isEVMChain(chainInfo.chainId)) {
|
||||
for (const privateKey of chainInfo.walletPrivateKey || []) {
|
||||
try {
|
||||
balancePromises.push(pullEVMNativeBalance(chainInfo, privateKey));
|
||||
} catch (e) {
|
||||
logger.error("pullEVMNativeBalance() failed: " + e);
|
||||
}
|
||||
}
|
||||
// TODO one day this will spin up independent watchers that time themselves
|
||||
// purposefully not awaited
|
||||
pullAllEVMTokens(env.supportedTokens, chainInfo, metrics);
|
||||
} else if (isTerraChain(chainInfo.chainId)) {
|
||||
// TODO one day this will spin up independent watchers that time themselves
|
||||
// purposefully not awaited
|
||||
pullAllTerraBalances(env.supportedTokens, chainInfo, metrics);
|
||||
} else {
|
||||
logger.error("Invalid chain ID in wallet monitor " + chainInfo.chainId);
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(
|
||||
"pulling balances failed failed for chain: " + chainInfo.chainName
|
||||
);
|
||||
if (e && e.stack) {
|
||||
logger.error(e.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const balancesArrays = await Promise.all(balancePromises);
|
||||
const balances = balancesArrays.reduce(
|
||||
(prev, curr) => [...prev, ...curr],
|
||||
[]
|
||||
);
|
||||
|
||||
return balances;
|
||||
}
|
||||
|
||||
export async function pullTerraBalance(
|
||||
lcd: LCDClient,
|
||||
walletAddress: string,
|
||||
tokenAddress: string,
|
||||
chainId: TerraChainId
|
||||
): Promise<WalletBalance | undefined> {
|
||||
try {
|
||||
const tokenInfo: any = await lcd.wasm.contractQuery(tokenAddress, {
|
||||
token_info: {},
|
||||
});
|
||||
const balanceInfo: any = await lcd.wasm.contractQuery(tokenAddress, {
|
||||
balance: {
|
||||
address: walletAddress,
|
||||
},
|
||||
});
|
||||
|
||||
if (!tokenInfo || !balanceInfo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
chainId,
|
||||
balanceAbs: balanceInfo?.balance?.toString() || "0",
|
||||
balanceFormatted: formatUnits(
|
||||
balanceInfo?.balance?.toString() || "0",
|
||||
tokenInfo.decimals
|
||||
),
|
||||
currencyName: tokenInfo.symbol,
|
||||
currencyAddressNative: tokenAddress,
|
||||
isNative: false,
|
||||
walletAddress: walletAddress,
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error("Failed to fetch terra balance for %s", tokenAddress);
|
||||
}
|
||||
}
|
||||
|
||||
async function pullSolanaTokenBalances(
|
||||
chainInfo: ChainConfigInfo,
|
||||
privateKey: Uint8Array
|
||||
): Promise<WalletBalance[]> {
|
||||
const keyPair = Keypair.fromSecretKey(privateKey);
|
||||
const connection = new Connection(chainInfo.nodeUrl);
|
||||
const output: WalletBalance[] = [];
|
||||
|
||||
try {
|
||||
const allAccounts = await connection.getParsedTokenAccountsByOwner(
|
||||
keyPair.publicKey,
|
||||
{ programId: TOKEN_PROGRAM_ID },
|
||||
"confirmed"
|
||||
);
|
||||
let mintAddresses: string[] = [];
|
||||
allAccounts.value.forEach((account) => {
|
||||
mintAddresses.push(account.account.data.parsed?.info?.mint);
|
||||
});
|
||||
const mdArray = await getMetaplexData(mintAddresses, chainInfo);
|
||||
|
||||
for (const account of allAccounts.value) {
|
||||
let mintAddress: string[] = [];
|
||||
mintAddress.push(account.account.data.parsed?.info?.mint);
|
||||
const mdArray = await getMetaplexData(mintAddress, chainInfo);
|
||||
let cName: string = "";
|
||||
if (mdArray && mdArray[0] && mdArray[0].data && mdArray[0].data.symbol) {
|
||||
const encoded = mdArray[0].data.symbol;
|
||||
cName = encodeURIComponent(encoded);
|
||||
cName = cName.replace(/%/g, "_");
|
||||
}
|
||||
|
||||
output.push({
|
||||
chainId: CHAIN_ID_SOLANA,
|
||||
balanceAbs: account.account.data.parsed?.info?.tokenAmount?.amount,
|
||||
balanceFormatted:
|
||||
account.account.data.parsed?.info?.tokenAmount?.uiAmount,
|
||||
currencyName: cName,
|
||||
currencyAddressNative: account.account.data.parsed?.info?.mint,
|
||||
isNative: false,
|
||||
walletAddress: account.pubkey.toString(),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("pullSolanaTokenBalances() - ", e);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function pullEVMNativeBalance(
|
||||
chainInfo: ChainConfigInfo,
|
||||
privateKey: string
|
||||
): Promise<WalletBalance[]> {
|
||||
if (!privateKey || !chainInfo.nodeUrl) {
|
||||
throw new Error("Bad chainInfo config for EVM chain: " + chainInfo.chainId);
|
||||
}
|
||||
|
||||
let provider = newProvider(chainInfo.nodeUrl);
|
||||
if (!provider) throw new Error("bad provider");
|
||||
const signer: Signer = new ethers.Wallet(privateKey, provider);
|
||||
const addr: string = await signer.getAddress();
|
||||
const weiAmount = await provider.getBalance(addr);
|
||||
const balanceInEth = ethers.utils.formatEther(weiAmount);
|
||||
|
||||
return [
|
||||
{
|
||||
chainId: chainInfo.chainId,
|
||||
balanceAbs: weiAmount.toString(),
|
||||
balanceFormatted: balanceInEth.toString(),
|
||||
currencyName: chainInfo.nativeCurrencySymbol,
|
||||
currencyAddressNative: "",
|
||||
isNative: true,
|
||||
walletAddress: addr,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function pullTerraNativeBalance(
|
||||
lcd: LCDClient,
|
||||
chainInfo: ChainConfigInfo,
|
||||
walletAddress: string
|
||||
): Promise<WalletBalance[]> {
|
||||
try {
|
||||
const output: WalletBalance[] = [];
|
||||
const [coins] = await lcd.bank.balance(walletAddress);
|
||||
// coins doesn't support reduce
|
||||
const balancePairs = coins.map(({ amount, denom }) => [denom, amount]);
|
||||
const balance = balancePairs.reduce((obj, current) => {
|
||||
obj[current[0].toString()] = current[1].toString();
|
||||
return obj;
|
||||
}, {} as TerraNativeBalances);
|
||||
Object.keys(balance).forEach((key) => {
|
||||
output.push({
|
||||
chainId: chainInfo.chainId,
|
||||
balanceAbs: balance[key],
|
||||
balanceFormatted: formatUnits(balance[key], 6).toString(),
|
||||
currencyName: formatNativeDenom(key, chainInfo.chainId as TerraChainId),
|
||||
currencyAddressNative: key,
|
||||
isNative: true,
|
||||
walletAddress: walletAddress,
|
||||
});
|
||||
});
|
||||
return output;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Failed to fetch terra native balances for wallet %s",
|
||||
walletAddress
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function pullSolanaNativeBalance(
|
||||
chainInfo: ChainConfigInfo,
|
||||
privateKey: Uint8Array
|
||||
): Promise<WalletBalance[]> {
|
||||
const keyPair = Keypair.fromSecretKey(privateKey);
|
||||
const connection = new Connection(chainInfo.nodeUrl);
|
||||
const fetchAccounts = await getMultipleAccountsRPC(connection, [
|
||||
keyPair.publicKey,
|
||||
]);
|
||||
|
||||
if (!fetchAccounts[0]) {
|
||||
//Accounts with zero balance report as not existing.
|
||||
return [
|
||||
{
|
||||
chainId: chainInfo.chainId,
|
||||
balanceAbs: "0",
|
||||
balanceFormatted: "0",
|
||||
currencyName: chainInfo.nativeCurrencySymbol,
|
||||
currencyAddressNative: chainInfo.chainName,
|
||||
isNative: true,
|
||||
walletAddress: keyPair.publicKey.toString(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const amountLamports = fetchAccounts[0].lamports.toString();
|
||||
const amountSol = formatUnits(
|
||||
fetchAccounts[0].lamports,
|
||||
WSOL_DECIMALS
|
||||
).toString();
|
||||
|
||||
return [
|
||||
{
|
||||
chainId: chainInfo.chainId,
|
||||
balanceAbs: amountLamports,
|
||||
balanceFormatted: amountSol,
|
||||
currencyName: chainInfo.nativeCurrencySymbol,
|
||||
currencyAddressNative: "",
|
||||
isNative: true,
|
||||
walletAddress: keyPair.publicKey.toString(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export async function collectWallets(metrics: PromHelper) {
|
||||
const scopedLogger = getScopedLogger(["collectWallets"], logger);
|
||||
const ONE_MINUTE: number = 60000;
|
||||
scopedLogger.info("Starting up.");
|
||||
while (true) {
|
||||
scopedLogger.debug("Pulling balances.");
|
||||
let wallets: WalletBalance[] = [];
|
||||
try {
|
||||
wallets = await pullBalances(metrics);
|
||||
} catch (e) {
|
||||
scopedLogger.error("Failed to pullBalances: " + e);
|
||||
}
|
||||
scopedLogger.debug("Done pulling balances.");
|
||||
metrics.handleWalletBalances(wallets);
|
||||
await sleep(ONE_MINUTE);
|
||||
}
|
||||
}
|
||||
|
||||
async function calcLocalAddressesEVM(
|
||||
provider: ethers.providers.JsonRpcBatchProvider,
|
||||
supportedTokens: SupportedToken[],
|
||||
chainConfigInfo: ChainConfigInfo
|
||||
): Promise<string[]> {
|
||||
const tokenBridge = Bridge__factory.connect(
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
provider
|
||||
);
|
||||
let tokenAddressPromises: Promise<string>[] = [];
|
||||
for (const supportedToken of supportedTokens) {
|
||||
if (supportedToken.chainId === chainConfigInfo.chainId) {
|
||||
tokenAddressPromises.push(Promise.resolve(supportedToken.address));
|
||||
continue;
|
||||
}
|
||||
const hexAddress = nativeToHexString(
|
||||
supportedToken.address,
|
||||
supportedToken.chainId
|
||||
);
|
||||
if (!hexAddress) {
|
||||
logger.debug(
|
||||
"calcLocalAddressesEVM() - no hexAddress for chainId: " +
|
||||
supportedToken.chainId +
|
||||
", address: " +
|
||||
supportedToken.address
|
||||
);
|
||||
continue;
|
||||
}
|
||||
tokenAddressPromises.push(
|
||||
tokenBridge.wrappedAsset(
|
||||
supportedToken.chainId,
|
||||
hexToUint8Array(hexAddress)
|
||||
)
|
||||
);
|
||||
}
|
||||
return (await Promise.all(tokenAddressPromises)).filter(
|
||||
(tokenAddress) =>
|
||||
tokenAddress && tokenAddress !== ethers.constants.AddressZero
|
||||
);
|
||||
}
|
||||
|
||||
export async function calcLocalAddressesTerra(
|
||||
lcd: LCDClient,
|
||||
supportedTokens: SupportedToken[],
|
||||
chainConfigInfo: ChainConfigInfo
|
||||
) {
|
||||
const output: string[] = [];
|
||||
for (const supportedToken of supportedTokens) {
|
||||
if (supportedToken.chainId === chainConfigInfo.chainId) {
|
||||
// skip natives, like uluna and uusd
|
||||
if (supportedToken.address.startsWith("terra")) {
|
||||
output.push(supportedToken.address);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const hexAddress = nativeToHexString(
|
||||
supportedToken.address,
|
||||
supportedToken.chainId
|
||||
);
|
||||
if (!hexAddress) {
|
||||
continue;
|
||||
}
|
||||
//This returns a native address
|
||||
let foreignAddress;
|
||||
try {
|
||||
foreignAddress = await getForeignAssetTerra(
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
lcd,
|
||||
supportedToken.chainId,
|
||||
hexToUint8Array(hexAddress)
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Foreign address exception.");
|
||||
}
|
||||
|
||||
if (!foreignAddress) {
|
||||
continue;
|
||||
}
|
||||
output.push(foreignAddress);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function pullAllEVMTokens(
|
||||
supportedTokens: SupportedToken[],
|
||||
chainConfig: ChainConfigInfo,
|
||||
metrics: PromHelper
|
||||
) {
|
||||
try {
|
||||
let provider = newProvider(
|
||||
chainConfig.nodeUrl,
|
||||
true
|
||||
) as ethers.providers.JsonRpcBatchProvider;
|
||||
const localAddresses = await calcLocalAddressesEVM(
|
||||
provider,
|
||||
supportedTokens,
|
||||
chainConfig
|
||||
);
|
||||
if (!chainConfig.walletPrivateKey) {
|
||||
return;
|
||||
}
|
||||
for (const privateKey of chainConfig.walletPrivateKey) {
|
||||
try {
|
||||
const publicAddress = await new ethers.Wallet(privateKey).getAddress();
|
||||
const tokens = await Promise.all(
|
||||
localAddresses.map((tokenAddress) =>
|
||||
getEthereumToken(tokenAddress, provider)
|
||||
)
|
||||
);
|
||||
const tokenInfos = await Promise.all(
|
||||
tokens.map((token) =>
|
||||
Promise.all([
|
||||
token.decimals(),
|
||||
token.balanceOf(publicAddress),
|
||||
token.symbol(),
|
||||
])
|
||||
)
|
||||
);
|
||||
const balances = tokenInfos.map(([decimals, balance, symbol], idx) => ({
|
||||
chainId: chainConfig.chainId,
|
||||
balanceAbs: balance.toString(),
|
||||
balanceFormatted: formatUnits(balance, decimals),
|
||||
currencyName: symbol,
|
||||
currencyAddressNative: localAddresses[idx],
|
||||
isNative: false,
|
||||
walletAddress: publicAddress,
|
||||
}));
|
||||
metrics.handleWalletBalances(balances);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"pullAllEVMTokens failed: for tokens " +
|
||||
JSON.stringify(localAddresses) +
|
||||
" on chain " +
|
||||
chainConfig.chainId +
|
||||
", error: " +
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"pullAllEVMTokens failed: for chain " +
|
||||
chainConfig.chainId +
|
||||
", error: " +
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function pullAllTerraBalances(
|
||||
supportedTokens: SupportedToken[],
|
||||
chainConfig: ChainConfigInfo,
|
||||
metrics: PromHelper
|
||||
) {
|
||||
let balances: WalletBalance[] = [];
|
||||
if (!chainConfig.walletPrivateKey) {
|
||||
return balances;
|
||||
}
|
||||
if (
|
||||
!(
|
||||
chainConfig.terraChainId &&
|
||||
chainConfig.terraCoin &&
|
||||
chainConfig.terraGasPriceUrl &&
|
||||
chainConfig.terraName
|
||||
)
|
||||
) {
|
||||
logger.error("Terra relay was called without proper instantiation.");
|
||||
throw new Error("Terra relay was called without proper instantiation.");
|
||||
}
|
||||
const lcdConfig = {
|
||||
URL: chainConfig.nodeUrl,
|
||||
chainID: chainConfig.terraChainId,
|
||||
name: chainConfig.terraName,
|
||||
isClassic: chainConfig.isTerraClassic,
|
||||
};
|
||||
const lcd = new LCDClient(lcdConfig);
|
||||
const localAddresses = await calcLocalAddressesTerra(
|
||||
lcd,
|
||||
supportedTokens,
|
||||
chainConfig
|
||||
);
|
||||
for (const privateKey of chainConfig.walletPrivateKey) {
|
||||
const mk = new MnemonicKey({
|
||||
mnemonic: privateKey,
|
||||
});
|
||||
const wallet = lcd.wallet(mk);
|
||||
const walletAddress = wallet.key.accAddress;
|
||||
balances = [
|
||||
...balances,
|
||||
...(await pullTerraNativeBalance(lcd, chainConfig, walletAddress)),
|
||||
];
|
||||
for (const address of localAddresses) {
|
||||
const balance = await pullTerraBalance(
|
||||
lcd,
|
||||
walletAddress,
|
||||
address,
|
||||
chainConfig.chainId as TerraChainId
|
||||
);
|
||||
if (balance) {
|
||||
balances.push(balance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metrics.handleWalletBalances(balances);
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
[
|
||||
{
|
||||
"chainId": 1,
|
||||
"privateKeys": [
|
||||
[
|
||||
14, 173, 153, 4, 176, 224, 201, 111, 32, 237, 183, 185, 159, 247, 22,
|
||||
161, 89, 84, 215, 209, 212, 137, 10, 92, 157, 49, 29, 192, 101, 164,
|
||||
152, 70, 87, 65, 8, 174, 214, 157, 175, 126, 98, 90, 54, 24, 100, 177,
|
||||
247, 77, 19, 112, 47, 44, 165, 109, 233, 102, 14, 86, 109, 29, 134, 145,
|
||||
132, 141
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": 2,
|
||||
"privateKeys": [
|
||||
"0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": 3,
|
||||
"privateKeys": [
|
||||
"notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": 4,
|
||||
"privateKeys": [
|
||||
"0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -1,118 +0,0 @@
|
|||
import {
|
||||
Bridge__factory,
|
||||
CHAIN_ID_CELO,
|
||||
CHAIN_ID_FANTOM,
|
||||
CHAIN_ID_KLAYTN,
|
||||
CHAIN_ID_POLYGON,
|
||||
getIsTransferCompletedEth,
|
||||
hexToUint8Array,
|
||||
redeemOnEth,
|
||||
redeemOnEthNative,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { ethers } from "ethers";
|
||||
import { ChainConfigInfo } from "../configureEnv";
|
||||
import { getScopedLogger, ScopedLogger } from "../helpers/logHelper";
|
||||
import { PromHelper } from "../helpers/promHelpers";
|
||||
import { CeloProvider, CeloWallet } from "@celo-tools/celo-ethers-wrapper";
|
||||
|
||||
export function newProvider(
|
||||
url: string,
|
||||
batch: boolean = false
|
||||
): ethers.providers.JsonRpcProvider | ethers.providers.JsonRpcBatchProvider {
|
||||
// only support http(s), not ws(s) as the websocket constructor can blow up the entire process
|
||||
// it uses a nasty setTimeout(()=>{},0) so we are unable to cleanly catch its errors
|
||||
if (url.startsWith("http")) {
|
||||
if (batch) {
|
||||
return new ethers.providers.JsonRpcBatchProvider(url);
|
||||
}
|
||||
return new ethers.providers.JsonRpcProvider(url);
|
||||
}
|
||||
throw new Error("url does not start with http/https!");
|
||||
}
|
||||
|
||||
export async function relayEVM(
|
||||
chainConfigInfo: ChainConfigInfo,
|
||||
signedVAA: string,
|
||||
unwrapNative: boolean,
|
||||
checkOnly: boolean,
|
||||
walletPrivateKey: string,
|
||||
relayLogger: ScopedLogger,
|
||||
metrics: PromHelper
|
||||
) {
|
||||
const logger = getScopedLogger(
|
||||
["evm", chainConfigInfo.chainName],
|
||||
relayLogger
|
||||
);
|
||||
const signedVaaArray = hexToUint8Array(signedVAA);
|
||||
let provider = undefined;
|
||||
let signer = undefined;
|
||||
if (chainConfigInfo.chainId === CHAIN_ID_CELO) {
|
||||
provider = new CeloProvider(chainConfigInfo.nodeUrl);
|
||||
await provider.ready;
|
||||
signer = new CeloWallet(walletPrivateKey, provider);
|
||||
} else {
|
||||
provider = newProvider(chainConfigInfo.nodeUrl);
|
||||
signer = new ethers.Wallet(walletPrivateKey, provider);
|
||||
}
|
||||
|
||||
logger.debug("Checking to see if vaa has already been redeemed.");
|
||||
const alreadyRedeemed = await getIsTransferCompletedEth(
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
provider,
|
||||
signedVaaArray
|
||||
);
|
||||
|
||||
if (alreadyRedeemed) {
|
||||
logger.info("VAA has already been redeemed!");
|
||||
return { redeemed: true, result: "already redeemed" };
|
||||
}
|
||||
if (checkOnly) {
|
||||
return { redeemed: false, result: "not redeemed" };
|
||||
}
|
||||
|
||||
if (unwrapNative) {
|
||||
logger.info(
|
||||
"Will redeem and unwrap using pubkey: %s",
|
||||
await signer.getAddress()
|
||||
);
|
||||
} else {
|
||||
logger.info("Will redeem using pubkey: %s", await signer.getAddress());
|
||||
}
|
||||
|
||||
logger.debug("Redeeming.");
|
||||
let overrides = {};
|
||||
if (chainConfigInfo.chainId === CHAIN_ID_POLYGON) {
|
||||
// look, there's something janky with Polygon + ethers + EIP-1559
|
||||
let feeData = await provider.getFeeData();
|
||||
overrides = {
|
||||
maxFeePerGas: feeData.maxFeePerGas?.mul(50) || undefined,
|
||||
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas?.mul(50) || undefined,
|
||||
};
|
||||
} else if (chainConfigInfo.chainId === CHAIN_ID_KLAYTN || chainConfigInfo.chainId === CHAIN_ID_FANTOM) {
|
||||
// Klaytn and Fantom require specifying gasPrice
|
||||
overrides = { gasPrice: (await signer.getGasPrice()).toString() };
|
||||
}
|
||||
const bridge = Bridge__factory.connect(
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
signer
|
||||
);
|
||||
const contractMethod = unwrapNative
|
||||
? bridge.completeTransferAndUnwrapETH
|
||||
: bridge.completeTransfer;
|
||||
const tx = await contractMethod(signedVaaArray, overrides);
|
||||
logger.info("waiting for tx hash: %s", tx.hash);
|
||||
const receipt = await tx.wait();
|
||||
|
||||
// Checking getIsTransferCompletedEth can be problematic if we get
|
||||
// load balanced to a node that is behind the block of our accepted tx
|
||||
// The auditor worker should confirm that our tx was successful
|
||||
const success = true;
|
||||
|
||||
if (provider instanceof ethers.providers.WebSocketProvider) {
|
||||
await provider.destroy();
|
||||
}
|
||||
|
||||
logger.info("success: %s tx hash: %s", success, receipt.transactionHash);
|
||||
metrics.incSuccesses(chainConfigInfo.chainId);
|
||||
return { redeemed: success, result: receipt };
|
||||
}
|
|
@ -1,305 +0,0 @@
|
|||
import { hexToUint8Array } from "@certusone/wormhole-sdk";
|
||||
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
|
||||
import { getRelayerEnvironment, RelayerEnvironment } from "../configureEnv";
|
||||
import { getLogger, getScopedLogger, ScopedLogger } from "../helpers/logHelper";
|
||||
import { PromHelper } from "../helpers/promHelpers";
|
||||
import {
|
||||
clearRedis,
|
||||
connectToRedis,
|
||||
demoteWorkingRedis,
|
||||
monitorRedis,
|
||||
RedisTables,
|
||||
Status,
|
||||
StorePayload,
|
||||
storePayloadFromJson,
|
||||
storePayloadToJson,
|
||||
WorkerInfo,
|
||||
} from "../helpers/redisHelper";
|
||||
import { sleep } from "../helpers/utils";
|
||||
import { getBackend } from "../backends";
|
||||
|
||||
const WORKER_THREAD_RESTART_MS = 10 * 1000;
|
||||
const AUDITOR_THREAD_RESTART_MS = 10 * 1000;
|
||||
const WORKER_INTERVAL_MS = 5 * 1000;
|
||||
|
||||
let metrics: PromHelper;
|
||||
|
||||
const logger = getLogger();
|
||||
let relayerEnv: RelayerEnvironment;
|
||||
|
||||
type WorkableItem = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export function init(): boolean {
|
||||
try {
|
||||
relayerEnv = getRelayerEnvironment();
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Encountered error while initiating the relayer environment: " + e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Initialize metrics for each chain and the worker infos */
|
||||
function createWorkerInfos(metrics: PromHelper): WorkerInfo[] {
|
||||
let workerArray: WorkerInfo[] = new Array();
|
||||
let index = 0;
|
||||
relayerEnv.supportedChains.forEach((chain) => {
|
||||
// initialize per chain metrics
|
||||
metrics.incSuccesses(chain.chainId, 0);
|
||||
metrics.incConfirmed(chain.chainId, 0);
|
||||
metrics.incFailures(chain.chainId, 0);
|
||||
metrics.incRollback(chain.chainId, 0);
|
||||
chain.walletPrivateKey?.forEach((key) => {
|
||||
workerArray.push({
|
||||
walletPrivateKey: key,
|
||||
index: index,
|
||||
targetChainId: chain.chainId,
|
||||
targetChainName: chain.chainName,
|
||||
});
|
||||
index++;
|
||||
});
|
||||
// TODO: Name the solanaprivatekey property the same as the non-solana one
|
||||
chain.solanaPrivateKey?.forEach((key) => {
|
||||
workerArray.push({
|
||||
walletPrivateKey: key,
|
||||
index: index,
|
||||
targetChainId: chain.chainId,
|
||||
targetChainName: chain.chainName,
|
||||
});
|
||||
index++;
|
||||
});
|
||||
});
|
||||
logger.info("will use " + workerArray.length + " workers");
|
||||
return workerArray;
|
||||
}
|
||||
|
||||
/** Spawn relay worker and auditor threads for all chains */
|
||||
async function spawnWorkerThreads(workerArray: WorkerInfo[]) {
|
||||
workerArray.forEach((workerInfo) => {
|
||||
spawnWorkerThread(workerInfo);
|
||||
spawnAuditorThread(workerInfo);
|
||||
});
|
||||
}
|
||||
/** Spawn an auditor thread for each (chain, wallet) combo from the backend implementation */
|
||||
async function spawnAuditorThread(workerInfo: WorkerInfo) {
|
||||
logger.info(
|
||||
`Spinning up auditor thread for target chain [${workerInfo.targetChainName}-${workerInfo.index}]`
|
||||
);
|
||||
|
||||
//At present, due to the try catch inside the while loop, this thread should never crash.
|
||||
const auditorPromise = getBackend()
|
||||
.relayer.runAuditor(workerInfo, metrics)
|
||||
.catch(async (error: Error) => {
|
||||
logger.error(
|
||||
`Fatal crash on auditor thread ${workerInfo.targetChainName}-${workerInfo.index}`
|
||||
);
|
||||
logger.error("error message: " + error.message);
|
||||
logger.error("error trace: " + error.stack);
|
||||
await sleep(AUDITOR_THREAD_RESTART_MS);
|
||||
spawnAuditorThread(workerInfo);
|
||||
});
|
||||
|
||||
return auditorPromise;
|
||||
}
|
||||
|
||||
export async function run(ph: PromHelper) {
|
||||
metrics = ph;
|
||||
|
||||
if (relayerEnv.clearRedisOnInit) {
|
||||
logger.info("Clearing REDIS as per tunable...");
|
||||
await clearRedis();
|
||||
} else if (relayerEnv.demoteWorkingOnInit) {
|
||||
logger.info("Demoting Working to Incoming as per tunable...");
|
||||
await demoteWorkingRedis();
|
||||
} else {
|
||||
logger.info("NOT clearing REDIS.");
|
||||
}
|
||||
|
||||
let workerArray: WorkerInfo[] = createWorkerInfos(metrics);
|
||||
|
||||
spawnWorkerThreads(workerArray);
|
||||
try {
|
||||
monitorRedis(metrics);
|
||||
} catch (e) {
|
||||
logger.error("Failed to kick off monitorRedis: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Redis does not guarantee ordering. Therefore, it is possible that if workItems are
|
||||
// pulled out one at a time, then some workItems could stay in the table indefinitely.
|
||||
// This function gathers all the items available at this moment to work on.
|
||||
async function findWorkableItems(
|
||||
workerInfo: WorkerInfo,
|
||||
relayLogger: ScopedLogger
|
||||
): Promise<WorkableItem[]> {
|
||||
const logger = getScopedLogger(["findWorkableItems"], relayLogger);
|
||||
try {
|
||||
let workableItems: WorkableItem[] = [];
|
||||
const redisClient = await connectToRedis();
|
||||
if (!redisClient) {
|
||||
logger.error("Failed to connect to redis inside findWorkableItems()!");
|
||||
return workableItems;
|
||||
}
|
||||
await redisClient.select(RedisTables.INCOMING);
|
||||
for await (const si_key of redisClient.scanIterator()) {
|
||||
const si_value = await redisClient.get(si_key);
|
||||
if (si_value) {
|
||||
let storePayload: StorePayload = storePayloadFromJson(si_value);
|
||||
// Check to see if this worker should handle this VAA
|
||||
if (workerInfo.targetChainId !== 0) {
|
||||
const { parse_vaa } = await importCoreWasm();
|
||||
const parsedVAA = parse_vaa(hexToUint8Array(storePayload.vaa_bytes));
|
||||
const payloadBuffer: Buffer = Buffer.from(parsedVAA.payload);
|
||||
const tgtChainId = getBackend().relayer.targetChainId(payloadBuffer);
|
||||
if (tgtChainId !== workerInfo.targetChainId) {
|
||||
// Skipping mismatched chainId
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if this is a retry and if it is time to retry
|
||||
if (storePayload.retries > 0) {
|
||||
const BACKOFF_TIME = 1000; // 1 second in milliseconds
|
||||
const MAX_BACKOFF_TIME = 4 * 60 * 60 * 1000; // 4 hours in milliseconds
|
||||
// calculate retry time
|
||||
const now: Date = new Date();
|
||||
const old: Date = new Date(storePayload.timestamp);
|
||||
const timeDelta: number = now.getTime() - old.getTime(); // delta is in mS
|
||||
const waitTime: number = Math.min(
|
||||
BACKOFF_TIME * 10 ** storePayload.retries, //First retry is 10 second, then 100, 1,000... Max of 4 hours.
|
||||
MAX_BACKOFF_TIME
|
||||
);
|
||||
if (timeDelta < waitTime) {
|
||||
// Not enough time has passed
|
||||
continue;
|
||||
}
|
||||
}
|
||||
workableItems.push({ key: si_key, value: si_value });
|
||||
}
|
||||
}
|
||||
redisClient.quit();
|
||||
return workableItems;
|
||||
} catch (e: any) {
|
||||
logger.error(
|
||||
"Recoverable exception scanning REDIS for workable items: " + e.message
|
||||
);
|
||||
logger.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Spin up one worker for each (chainId, privateKey) combo. */
|
||||
async function spawnWorkerThread(workerInfo: WorkerInfo) {
|
||||
logger.info(
|
||||
"Spinning up worker[" +
|
||||
workerInfo.index +
|
||||
"] to handle target chain " +
|
||||
workerInfo.targetChainId +
|
||||
` / ${workerInfo.targetChainName}`
|
||||
);
|
||||
|
||||
const workerPromise = doWorkerThread(workerInfo).catch(async (error) => {
|
||||
logger.error(
|
||||
"Fatal crash on worker thread: index " +
|
||||
workerInfo.index +
|
||||
" chainId " +
|
||||
workerInfo.targetChainId
|
||||
);
|
||||
logger.error("error message: " + error.message);
|
||||
logger.error("error trace: " + error.stack);
|
||||
await sleep(WORKER_THREAD_RESTART_MS);
|
||||
spawnWorkerThread(workerInfo);
|
||||
});
|
||||
|
||||
return workerPromise;
|
||||
}
|
||||
|
||||
async function doWorkerThread(workerInfo: WorkerInfo) {
|
||||
// relay-worker-solana-1
|
||||
const loggerName = `relay-worker-${workerInfo.targetChainName}-${workerInfo.index}`;
|
||||
const relayLogger = getScopedLogger([loggerName]);
|
||||
const backend = getBackend().relayer;
|
||||
while (true) {
|
||||
// relayLogger.debug("Finding workable items.");
|
||||
const workableItems: WorkableItem[] = await findWorkableItems(
|
||||
workerInfo,
|
||||
relayLogger
|
||||
);
|
||||
// relayLogger.debug("Found items: %o", workableItems);
|
||||
let i: number = 0;
|
||||
for (i = 0; i < workableItems.length; i++) {
|
||||
const workItem: WorkableItem = workableItems[i];
|
||||
if (workItem) {
|
||||
//This will attempt to move the workable item to the WORKING table
|
||||
relayLogger.debug("Moving item: %o", workItem);
|
||||
if (await moveToWorking(workItem, relayLogger)) {
|
||||
relayLogger.info("Moved key to WORKING table: %s", workItem.key);
|
||||
await backend.process(
|
||||
workItem.key,
|
||||
workerInfo.walletPrivateKey,
|
||||
relayLogger,
|
||||
metrics
|
||||
);
|
||||
} else {
|
||||
relayLogger.error(
|
||||
"Cannot move work item from INCOMING to WORKING: %s",
|
||||
workItem.key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// relayLogger.debug(
|
||||
// "Taking a break for %i seconds",
|
||||
// WORKER_INTERVAL_MS / 1000
|
||||
// );
|
||||
await sleep(WORKER_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
async function moveToWorking(
|
||||
workItem: WorkableItem,
|
||||
relayLogger: ScopedLogger
|
||||
): Promise<boolean> {
|
||||
const logger = getScopedLogger(["moveToWorking"], relayLogger);
|
||||
try {
|
||||
const redisClient = await connectToRedis();
|
||||
if (!redisClient) {
|
||||
logger.error("Failed to connect to Redis.");
|
||||
return false;
|
||||
}
|
||||
// Move this entry from incoming store to working store
|
||||
await redisClient.select(RedisTables.INCOMING);
|
||||
if ((await redisClient.del(workItem.key)) === 0) {
|
||||
logger.info("The key %s no longer exists in INCOMING", workItem.key);
|
||||
await redisClient.quit();
|
||||
return false;
|
||||
}
|
||||
await redisClient.select(RedisTables.WORKING);
|
||||
// If this VAA is already in the working store, then no need to add it again.
|
||||
// This handles the case of duplicate VAAs from multiple guardians
|
||||
const checkVal = await redisClient.get(workItem.key);
|
||||
if (!checkVal) {
|
||||
let payload: StorePayload = storePayloadFromJson(workItem.value);
|
||||
payload.status = Status.Pending;
|
||||
await redisClient.set(workItem.key, storePayloadToJson(payload));
|
||||
await redisClient.quit();
|
||||
return true;
|
||||
} else {
|
||||
metrics.incAlreadyExec();
|
||||
logger.debug("Dropping request %s as already processed", workItem.key);
|
||||
await redisClient.quit();
|
||||
return false;
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error("Recoverable exception moving item to working: " + e.message);
|
||||
logger.error("%s => %s", workItem.key, workItem.value);
|
||||
logger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
import {
|
||||
CHAIN_ID_SOLANA,
|
||||
getForeignAssetSolana,
|
||||
getIsTransferCompletedSolana,
|
||||
hexToUint8Array,
|
||||
importCoreWasm,
|
||||
parseTransferPayload,
|
||||
postVaaSolanaWithRetry,
|
||||
redeemOnSolana,
|
||||
tryHexToNativeAssetString,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { ChainConfigInfo } from "../configureEnv";
|
||||
import { getScopedLogger, ScopedLogger } from "../helpers/logHelper";
|
||||
import { PromHelper } from "../helpers/promHelpers";
|
||||
|
||||
const MAX_VAA_UPLOAD_RETRIES_SOLANA = 5;
|
||||
|
||||
export async function relaySolana(
|
||||
chainConfigInfo: ChainConfigInfo,
|
||||
signedVAAString: string,
|
||||
checkOnly: boolean,
|
||||
walletPrivateKey: Uint8Array,
|
||||
relayLogger: ScopedLogger,
|
||||
metrics: PromHelper
|
||||
) {
|
||||
const logger = getScopedLogger(["solana"], relayLogger);
|
||||
//TODO native transfer & create associated token account
|
||||
//TODO close connection
|
||||
const signedVaaArray = hexToUint8Array(signedVAAString);
|
||||
const signedVaaBuffer = Buffer.from(signedVaaArray);
|
||||
const connection = new Connection(chainConfigInfo.nodeUrl, "confirmed");
|
||||
|
||||
if (!chainConfigInfo.bridgeAddress) {
|
||||
// This should never be the case, as enforced by createSolanaChainConfig
|
||||
return { redeemed: false, result: null };
|
||||
}
|
||||
|
||||
const keypair = Keypair.fromSecretKey(walletPrivateKey);
|
||||
const payerAddress = keypair.publicKey.toString();
|
||||
|
||||
logger.info(
|
||||
"publicKey: %s, bridgeAddress: %s, tokenBridgeAddress: %s",
|
||||
payerAddress,
|
||||
chainConfigInfo.bridgeAddress,
|
||||
chainConfigInfo.tokenBridgeAddress
|
||||
);
|
||||
logger.debug("Checking to see if vaa has already been redeemed.");
|
||||
|
||||
const alreadyRedeemed = await getIsTransferCompletedSolana(
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
signedVaaArray,
|
||||
connection
|
||||
);
|
||||
|
||||
if (alreadyRedeemed) {
|
||||
logger.info("VAA has already been redeemed!");
|
||||
return { redeemed: true, result: "already redeemed" };
|
||||
}
|
||||
if (checkOnly) {
|
||||
return { redeemed: false, result: "not redeemed" };
|
||||
}
|
||||
|
||||
// determine fee destination address - an associated token account
|
||||
const { parse_vaa } = await importCoreWasm();
|
||||
const parsedVAA = parse_vaa(signedVaaArray);
|
||||
const payloadBuffer = Buffer.from(parsedVAA.payload);
|
||||
const transferPayload = parseTransferPayload(payloadBuffer);
|
||||
logger.debug("Calculating the fee destination address");
|
||||
let nativeOrigin: string;
|
||||
|
||||
try {
|
||||
nativeOrigin = tryHexToNativeAssetString(
|
||||
transferPayload.originAddress,
|
||||
CHAIN_ID_SOLANA
|
||||
);
|
||||
} catch (e: any) {
|
||||
throw new Error(
|
||||
`Unable to convert origin address to native: ${e?.message}`
|
||||
);
|
||||
}
|
||||
|
||||
const solanaMintAddress =
|
||||
transferPayload.originChain === CHAIN_ID_SOLANA
|
||||
? nativeOrigin
|
||||
: await getForeignAssetSolana(
|
||||
connection,
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
transferPayload.originChain,
|
||||
hexToUint8Array(transferPayload.originAddress)
|
||||
);
|
||||
if (!solanaMintAddress) {
|
||||
throw new Error(
|
||||
`Unable to determine mint for origin chain: ${transferPayload.originChain}, address: ${transferPayload.originAddress} (${nativeOrigin})`
|
||||
);
|
||||
}
|
||||
const solanaMintKey = new PublicKey(solanaMintAddress);
|
||||
const feeRecipientAddress = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
solanaMintKey,
|
||||
keypair.publicKey
|
||||
);
|
||||
// create the associated token account if it doesn't exist
|
||||
const associatedAddressInfo = await connection.getAccountInfo(
|
||||
feeRecipientAddress
|
||||
);
|
||||
if (!associatedAddressInfo) {
|
||||
logger.debug(
|
||||
"Fee destination address %s for wallet %s, mint %s does not exist, creating it.",
|
||||
feeRecipientAddress.toString(),
|
||||
keypair.publicKey,
|
||||
solanaMintAddress
|
||||
);
|
||||
const transaction = new Transaction().add(
|
||||
await Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
solanaMintKey,
|
||||
feeRecipientAddress,
|
||||
keypair.publicKey, // owner
|
||||
keypair.publicKey // payer
|
||||
)
|
||||
);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = keypair.publicKey;
|
||||
// sign, send, and confirm transaction
|
||||
transaction.partialSign(keypair);
|
||||
const txid = await connection.sendRawTransaction(transaction.serialize());
|
||||
await connection.confirmTransaction(txid);
|
||||
}
|
||||
|
||||
logger.debug("Posting the vaa.");
|
||||
await postVaaSolanaWithRetry(
|
||||
connection,
|
||||
async (transaction) => {
|
||||
transaction.partialSign(keypair);
|
||||
return transaction;
|
||||
},
|
||||
chainConfigInfo.bridgeAddress,
|
||||
payerAddress,
|
||||
signedVaaBuffer,
|
||||
MAX_VAA_UPLOAD_RETRIES_SOLANA
|
||||
);
|
||||
|
||||
logger.debug("Redeeming.");
|
||||
const unsignedTransaction = await redeemOnSolana(
|
||||
connection,
|
||||
chainConfigInfo.bridgeAddress,
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
payerAddress,
|
||||
signedVaaArray,
|
||||
feeRecipientAddress.toString()
|
||||
);
|
||||
|
||||
logger.debug("Sending.");
|
||||
unsignedTransaction.partialSign(keypair);
|
||||
const txid = await connection.sendRawTransaction(
|
||||
unsignedTransaction.serialize()
|
||||
);
|
||||
await connection.confirmTransaction(txid);
|
||||
|
||||
logger.debug("Checking to see if the transaction is complete.");
|
||||
const success = await getIsTransferCompletedSolana(
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
signedVaaArray,
|
||||
connection
|
||||
);
|
||||
|
||||
logger.info("success: %s, tx hash: %s", success, txid);
|
||||
metrics.incSuccesses(chainConfigInfo.chainId);
|
||||
return { redeemed: success, result: txid };
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
import {
|
||||
getIsTransferCompletedTerra,
|
||||
hexToUint8Array,
|
||||
redeemOnTerra,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { LCDClient, MnemonicKey } from "@terra-money/terra.js";
|
||||
import axios from "axios";
|
||||
import { ChainConfigInfo } from "../configureEnv";
|
||||
import { getScopedLogger, ScopedLogger } from "../helpers/logHelper";
|
||||
import { PromHelper } from "../helpers/promHelpers";
|
||||
|
||||
export async function relayTerra(
|
||||
chainConfigInfo: ChainConfigInfo,
|
||||
signedVAA: string,
|
||||
checkOnly: boolean,
|
||||
walletPrivateKey: any,
|
||||
relayLogger: ScopedLogger,
|
||||
metrics: PromHelper
|
||||
) {
|
||||
const logger = getScopedLogger(["terra"], relayLogger);
|
||||
if (
|
||||
!(
|
||||
chainConfigInfo.terraChainId &&
|
||||
chainConfigInfo.terraCoin &&
|
||||
chainConfigInfo.terraGasPriceUrl &&
|
||||
chainConfigInfo.terraName
|
||||
)
|
||||
) {
|
||||
logger.error("Terra relay was called without proper instantiation.");
|
||||
throw new Error("Terra relay was called without proper instantiation.");
|
||||
}
|
||||
const signedVaaArray = hexToUint8Array(signedVAA);
|
||||
const lcdConfig = {
|
||||
URL: chainConfigInfo.nodeUrl,
|
||||
chainID: chainConfigInfo.terraChainId,
|
||||
name: chainConfigInfo.terraName,
|
||||
isClassic: chainConfigInfo.isTerraClassic,
|
||||
};
|
||||
const lcd = new LCDClient(lcdConfig);
|
||||
const mk = new MnemonicKey({
|
||||
mnemonic: walletPrivateKey,
|
||||
});
|
||||
const wallet = lcd.wallet(mk);
|
||||
|
||||
logger.info(
|
||||
"terraChainId: %s, tokenBridgeAddress: %s, accAddress: %s, signedVAA: %s",
|
||||
chainConfigInfo.terraChainId,
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
wallet.key.accAddress,
|
||||
signedVAA
|
||||
);
|
||||
|
||||
logger.debug("Checking to see if vaa has already been redeemed.");
|
||||
const alreadyRedeemed = await getIsTransferCompletedTerra(
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
signedVaaArray,
|
||||
lcd,
|
||||
chainConfigInfo.terraGasPriceUrl
|
||||
);
|
||||
|
||||
if (alreadyRedeemed) {
|
||||
logger.info("VAA has already been redeemed!");
|
||||
return { redeemed: true, result: "already redeemed" };
|
||||
}
|
||||
if (checkOnly) {
|
||||
return { redeemed: false, result: "not redeemed" };
|
||||
}
|
||||
|
||||
const msg = await redeemOnTerra(
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
wallet.key.accAddress,
|
||||
signedVaaArray
|
||||
);
|
||||
|
||||
logger.debug("Getting gas prices");
|
||||
//let gasPrices = await lcd.config.gasPrices //Unsure if the values returned from this are hardcoded or not.
|
||||
//Thus, we are going to pull it directly from the current FCD.
|
||||
const gasPrices = await axios
|
||||
.get(chainConfigInfo.terraGasPriceUrl)
|
||||
.then((result) => result.data);
|
||||
|
||||
logger.debug("Estimating fees");
|
||||
const account = await lcd.auth.accountInfo(wallet.key.accAddress);
|
||||
const feeEstimate = await lcd.tx.estimateFee(
|
||||
[
|
||||
{
|
||||
sequenceNumber: account.getSequenceNumber(),
|
||||
publicKey: account.getPublicKey(),
|
||||
},
|
||||
],
|
||||
{
|
||||
msgs: [msg],
|
||||
feeDenoms: [chainConfigInfo.terraCoin],
|
||||
gasPrices,
|
||||
}
|
||||
);
|
||||
|
||||
logger.debug("createAndSign");
|
||||
const tx = await wallet.createAndSignTx({
|
||||
msgs: [msg],
|
||||
memo: "Relayer - Complete Transfer",
|
||||
feeDenoms: [chainConfigInfo.terraCoin],
|
||||
gasPrices,
|
||||
fee: feeEstimate,
|
||||
});
|
||||
|
||||
logger.debug("Broadcasting");
|
||||
const receipt = await lcd.tx.broadcast(tx);
|
||||
|
||||
logger.debug("Checking to see if the transaction is complete.");
|
||||
const success = await getIsTransferCompletedTerra(
|
||||
chainConfigInfo.tokenBridgeAddress,
|
||||
signedVaaArray,
|
||||
lcd,
|
||||
chainConfigInfo.terraGasPriceUrl
|
||||
);
|
||||
|
||||
logger.info("success: %s, tx hash: %s", success, receipt.txhash);
|
||||
metrics.incSuccesses(chainConfigInfo.chainId);
|
||||
return { redeemed: success, result: receipt.txhash };
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { TokenImplementation__factory } from "@certusone/wormhole-sdk";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
export async function getEthereumToken(
|
||||
tokenAddress: string,
|
||||
provider: ethers.providers.Provider
|
||||
) {
|
||||
const token = TokenImplementation__factory.connect(tokenAddress, provider);
|
||||
return token;
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js";
|
||||
|
||||
export async function getMultipleAccountsRPC(
|
||||
connection: Connection,
|
||||
pubkeys: PublicKey[]
|
||||
): Promise<(AccountInfo<Buffer> | null)[]> {
|
||||
return getMultipleAccounts(connection, pubkeys, "confirmed");
|
||||
}
|
||||
|
||||
export const getMultipleAccounts = async (
|
||||
connection: any,
|
||||
pubkeys: PublicKey[],
|
||||
commitment: string
|
||||
) => {
|
||||
return (
|
||||
await Promise.all(
|
||||
chunks(pubkeys, 99).map((chunk) =>
|
||||
connection.getMultipleAccountsInfo(chunk, commitment)
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
};
|
||||
|
||||
export function chunks<T>(array: T[], size: number): T[][] {
|
||||
return Array.apply<number, T[], T[][]>(
|
||||
0,
|
||||
new Array(Math.ceil(array.length / size))
|
||||
).map((_, index) => array.slice(index * size, (index + 1) * size));
|
||||
}
|
||||
|
||||
export function shortenAddress(address: string) {
|
||||
return address.length > 10
|
||||
? `${address.slice(0, 4)}...${address.slice(-4)}`
|
||||
: address;
|
||||
}
|
||||
|
||||
export const WSOL_DECIMALS = 9;
|
|
@ -1,23 +0,0 @@
|
|||
import {
|
||||
CHAIN_ID_TERRA2,
|
||||
isNativeTerra,
|
||||
TerraChainId,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
|
||||
export const LUNA_SYMBOL = "LUNA";
|
||||
export const LUNA_CLASSIC_SYMBOL = "LUNC";
|
||||
|
||||
export const formatNativeDenom = (
|
||||
denom: string,
|
||||
chainId: TerraChainId
|
||||
): string => {
|
||||
const unit = denom.slice(1).toUpperCase();
|
||||
const isValidTerra = isNativeTerra(denom);
|
||||
return denom === "uluna"
|
||||
? chainId === CHAIN_ID_TERRA2
|
||||
? LUNA_SYMBOL
|
||||
: LUNA_CLASSIC_SYMBOL
|
||||
: isValidTerra
|
||||
? unit.slice(0, 2) + "TC"
|
||||
: "";
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
import { ChainId } from "@certusone/wormhole-sdk";
|
||||
|
||||
export const chainIDStrings: { [key in ChainId]: string } = {
|
||||
0: "Unset",
|
||||
1: "Solana",
|
||||
2: "Ethereum",
|
||||
3: "Terra",
|
||||
4: "BSC",
|
||||
5: "Polygon",
|
||||
6: "Avalanche",
|
||||
7: "Oasis",
|
||||
8: "Algorand",
|
||||
9: "Aurora",
|
||||
10: "Fantom",
|
||||
11: "Karura",
|
||||
12: "Acala",
|
||||
13: "Klaytn",
|
||||
14: "Celo",
|
||||
15: "NEAR",
|
||||
16: "Moonbeam",
|
||||
17: "Neon",
|
||||
18: "Terra2",
|
||||
10001: "Ropsten",
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["es2019"],
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"downlevelIteration": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/__tests__/*"]
|
||||
}
|
Loading…
Reference in New Issue