Modular relayer support (#1266)

* Add the default backend for the relayer

Start to slowly split things out.

* Configure the backends when configuring the env

* First stab at the pluggable listener backend

* Update relayer example (and test) config

* relayer: JIT backend

* relayer: walletMonitor remove useless function

* relayer: update worker init() funcs

It is silly to accept a boolean in thing.init() vs just not running init
if you don't want to run thing.

* relayer: remove env property from Listener

It is verified in init() in spy_listener.ts and rest_listen.ts, so it can
be deferred to not require the import.

* relayer: clean up the main bits

Only try to init() something when it is actually supposed to run
per the cli flags.

* spy_relayer: more descriptive var name in main

* spy_relayer: Update relay worker with more docs

* spy_relayer: add targetChainName to WorkerInfo

This makes it easier to pretty print the chain name in the logs without
having to look the name up.

* spy_relayer: update logs and use the backend

* Use the Relayer interface's process() method in place of processVaa()
* Update the logs to include the chain name in the worker and auditor threads

* spy_relayer: remove processRequest()

It has been moved to the process() method of the default Relayer backend.

* spy_relayer: start fleshing out the Relayer default

* spy_relayer: fix a logic bug in checkQueue()

Co-authored-by: @swimricky

* spy_relayer: update TokenBridgeRelayer.process()

* Remove some extra logic
* Actually use the ChainId type since the id of 0 is in the sdk now

* spy_signature: add Relayer.runAuditor()

The auditor code is payload specific and needs to be with the backend.

* spy_relayer: move Relayer.runAuditor()

Make it part of the backend since the backend has all of the payload
specific logic into it.

* spy_relayer: move relay() --> Relayer.relay()

The actual relaying is part of the backend, so make it so.

* spy_relayer: add Relayer.runAuditor()

* spy_relayer: no more deprecated hexToNativeString

* spy_relayer: implement Relayer.targetChainId()

This is used for finding workable items in the incoming queue to toss
into the working queue.

* spy_relayer: remove relay.ts

The relay() function was moved to Relayer.relay()

* spy_relayer: more uses of deprecated hexToNativeString()

* spy_relayer: remove unused import

* spy_relayer: review feedback from @bruce-riley

* Fix some spelling tyops
* Simplify some logging
* Simplify a conditional for the payload version check

* spy_relayer: misc fixes and code clean up

* Fixed integration tests
* Added launch.json file for debugging in VS Code
* Updated to latest wormhole SDK
* Backup queue uses same key as redis
* Added Terra Classic flag
* Throttle potential infinite loop in audit thread
* Fixed spy service connection leak

Co-authored-by: Evan Gray <battledingo@gmail.com>
Co-authored-by: Kevin Peters <kpeters@jumptrading.com>
This commit is contained in:
Jeff Schroeder 2022-08-05 09:20:36 -05:00 committed by GitHub
parent 2c52f86546
commit bb4d2ac206
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1285 additions and 1141 deletions

View File

@ -1,19 +1,14 @@
SUPPORTED_CHAINS=[ { "chainId": 1, "chainName": "Solana", "nodeUrl": "http://solana-devnet:8899", "tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16", "bridgeAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o", "walletPrivateKey": [ [ 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 ] ], "wrappedAsset": "So11111111111111111111111111111111111111112" }, { "chainId": 2, "chainName": "ETH", "nodeUrl": "http://eth-devnet:8545", "tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16", "walletPrivateKey": [ "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" ], "wrappedAsset": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E" }, { "chainId": 3, "chainName": "Terra", "nodeUrl": "http://terra-terrad:1317", "tokenBridgeAddress": "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4", "walletPrivateKey": [ "notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius" ], "terraName": "localterra", "terraChainId": "columbus-5", "terraCoin": "uluna", "terraGasPriceUrl": "http://terra-fcd:3060/v1/txs/gas_prices" }, { "chainId": 4, "chainName": "BSC", "nodeUrl": "http://eth-devnet2:8545", "tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16", "walletPrivateKey": [ "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" ], "wrappedAsset": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E" }]
REDIS_HOST= redis
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=false
CLEAR_REDIS_ON_INIT=true
DEMOTE_WORKING_ON_INIT=true
LOG_LEVEL=debug
SUPPORTED_TOKENS=[{"chainId":1,"address":"So11111111111111111111111111111111111111112"}, {"chainId":2,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"}, {"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=spy:7072
SPY_SERVICE_FILTERS=[{"chainId":1,"emitterAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"}, {"chainId":2,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"}, {"chainId":3,"emitterAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"}, {"chainId":4,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"}]
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

View File

@ -1,8 +1,5 @@
SPY_SERVICE_HOST=spy:7072
#Solana mainnet emitter address Gv1KWf8DT1jKv5pKBmGaTmVszqa56Xn8YGx2Pg7i7qAk
#Devnet emitter address? ENG1wQ7CQKH8ibAJ1hSLmJgL9Ucg6DRDbj752ZAfidLA
#Devnet token bridge address: B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE
SPY_SERVICE_FILTERS=[{"chainId":1,"emitterAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"}, {"chainId":2,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"}, {"chainId":3,"emitterAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"}, {"chainId":4,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"}]
SPY_SERVICE_FILTERS=[{"chainId":1,"emitterAddress":"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"},{"chainId":2,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"},{"chainId":3,"emitterAddress":"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"},{"chainId":4,"emitterAddress":"0x0290FB167208Af455bB137780163b7B7a9a10C16"}]
SPY_NUM_WORKERS=5
REDIS_HOST=redis
@ -11,8 +8,6 @@ REDIS_PORT=6379
REST_PORT=4201
PROM_PORT=8082
READINESS_PORT=2000
#TODO change this to an array of numbers
#SPY_MIN_FEES = 500000
LOG_LEVEL=debug
SUPPORTED_TOKENS=[{"chainId":1,"address":"So11111111111111111111111111111111111111112"}, {"chainId":2,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"}, {"chainId":3,"address":"uluna"}, {"chainId":4,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"}]
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"}]

View File

@ -1,10 +1,10 @@
SUPPORTED_CHAINS=[ { "chainId": 1, "chainName": "Solana", "nativeCurrencySymbol": "SOL", "nodeUrl": "http://solana-devnet:8899", "tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16", "bridgeAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o", "walletPrivateKey": [ [ 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 ] ], "wrappedAsset": "So11111111111111111111111111111111111111112" }, { "chainId": 2, "chainName": "Ethereum", "nativeCurrencySymbol": "ETH", "nodeUrl": "http://eth-devnet:8545", "tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16", "walletPrivateKey": [ "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" ], "wrappedAsset": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E" }, { "chainId": 3, "chainName": "Terra", "nativeCurrencySymbol": "LUNA", "nodeUrl": "http://terra-terrad:1317", "tokenBridgeAddress": "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4", "walletPrivateKey": [ "notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius" ], "terraName": "localterra", "terraChainId": "columbus-5", "terraCoin": "uluna", "terraGasPriceUrl": "http://terra-fcd:3060/v1/txs/gas_prices" }, { "chainId": 4, "chainName": "Binance Smart Chain", "nativeCurrencySymbol": "BNB", "nodeUrl": "http://eth-devnet2:8545", "tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16", "walletPrivateKey": [ "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" ], "wrappedAsset": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E" }]
REDIS_HOST= redis
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","isTerraClassic":true,"nativeCurrencySymbol":"LUNA","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"}]
REDIS_HOST=redis
REDIS_PORT=6379
PROM_PORT=8083
READINESS_PORT=2000
CLEAR_REDIS_ON_INIT=false
CLEAR_REDIS_ON_INIT=true
DEMOTE_WORKING_ON_INIT=true
LOG_LEVEL=debug
SUPPORTED_TOKENS=[{"chainId":1,"address":"So11111111111111111111111111111111111111112"}, {"chainId":2,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"}, {"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" ] }]
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"]}]

View File

@ -1,10 +1,10 @@
SUPPORTED_CHAINS=[ { "chainId": 1, "chainName": "Solana", "nativeCurrencySymbol": "SOL", "nodeUrl": "http://solana-devnet:8899", "tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16", "bridgeAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o", "walletPrivateKey": [ [ 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 ] ], "wrappedAsset": "So11111111111111111111111111111111111111112" }, { "chainId": 2, "chainName": "Ethereum", "nativeCurrencySymbol": "ETH", "nodeUrl": "http://eth-devnet:8545", "tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16", "walletPrivateKey": [ "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" ], "wrappedAsset": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E" }, { "chainId": 3, "chainName": "Terra", "nativeCurrencySymbol": "LUNA", "nodeUrl": "http://terra-terrad:1317", "tokenBridgeAddress": "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4", "walletPrivateKey": [ "notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius" ], "terraName": "localterra", "terraChainId": "columbus-5", "terraCoin": "uluna", "terraGasPriceUrl": "http://terra-fcd:3060/v1/txs/gas_prices" }, { "chainId": 4, "chainName": "Binance Smart Chain", "nativeCurrencySymbol": "BNB", "nodeUrl": "http://eth-devnet2:8545", "tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16", "walletPrivateKey": [ "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" ], "wrappedAsset": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E" }]
REDIS_HOST= redis
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","isTerraClassic":true,"nativeCurrencySymbol":"LUNA","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"}]
REDIS_HOST=redis
REDIS_PORT=6379
PROM_PORT=8084
READINESS_PORT=2000
CLEAR_REDIS_ON_INIT=false
CLEAR_REDIS_ON_INIT=true
DEMOTE_WORKING_ON_INIT=true
LOG_LEVEL=debug
SUPPORTED_TOKENS=[{"chainId":1,"address":"So11111111111111111111111111111111111111112"}, {"chainId":2,"address":"0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"}, {"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" ] }]
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"]}]

18
relayer/spy_relayer/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
// 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"
}
]
}

View File

@ -1,3 +0,0 @@
Test Scenarios:
- Should survive any container restarting: Spy, Listener, Redis, or Relayer.

View File

@ -28,7 +28,7 @@ Validation criteria:
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 successfuly submitted on chain.
- 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.

View File

@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"@celo-tools/celo-ethers-wrapper": "^0.1.0",
"@certusone/wormhole-sdk": "^0.3.8",
"@certusone/wormhole-sdk": "^0.5.0",
"@certusone/wormhole-spydk": "^0.0.1",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
@ -731,22 +731,74 @@
}
},
"node_modules/@certusone/wormhole-sdk": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.3.8.tgz",
"integrity": "sha512-XHWKRqYo+Euj8DNWPLM3j5cXAqVkp4QtmpYqheLqO3hQLIc5MvyHZfXHxfAq0gmrdQT89xP5Dq8Q3uLGtq07QQ==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.5.0.tgz",
"integrity": "sha512-Z8Cj2yZ41if842jSSLzKLomwkq9PgXdjVq3r3VNzkSM3aZavU8vZqNT33LU9IabmW7hiWe1uI9j2Z1JZe7SIEg==",
"dependencies": {
"@improbable-eng/grpc-web": "^0.14.0",
"@certusone/wormhole-sdk-proto-web": "^0.0.1",
"@certusone/wormhole-sdk-wasm": "^0.0.1",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^3.0.7",
"@terra-money/terra.js": "^3.1.3",
"algosdk": "^1.15.0",
"axios": "^0.24.0",
"bech32": "^2.0.0",
"js-base64": "^3.6.1",
"protobufjs": "^6.11.2",
"rxjs": "^7.3.0"
"js-base64": "^3.6.1"
}
},
"node_modules/@certusone/wormhole-sdk-proto-web": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-proto-web/-/wormhole-sdk-proto-web-0.0.1.tgz",
"integrity": "sha512-v6D+vCPqzTmrRuN0ZHpOdA1XnF3nmaD1wlJf025SXb7JFhVSmKyFXzLajkt50rk6SCkEvXtRlxNTJtnuCxg94Q==",
"dependencies": {
"@improbable-eng/grpc-web": "^0.15.0",
"protobufjs": "^7.0.0",
"rxjs": "^7.5.6"
}
},
"node_modules/@certusone/wormhole-sdk-proto-web/node_modules/long": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
},
"node_modules/@certusone/wormhole-sdk-proto-web/node_modules/protobufjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.0.0.tgz",
"integrity": "sha512-ffNIEm+quOcYtQvHdW406v1NQmZSuqVklxsXk076BtuFnlYZfigLU+JOMrTD8TUOyqHYbRI/fSVNvgd25YeN3w==",
"hasInstallScript": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@certusone/wormhole-sdk-wasm": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-wasm/-/wormhole-sdk-wasm-0.0.1.tgz",
"integrity": "sha512-LdIwLhOyr4pPs2jqYubqC7d4UkqYBX0EG/ppspQlW3qlVE0LZRMrH6oVzzLMyHtV0Rw7O9sIKzORW/T3mrJv2w==",
"dependencies": {
"@types/long": "^4.0.2",
"@types/node": "^18.0.3"
}
},
"node_modules/@certusone/wormhole-sdk-wasm/node_modules/@types/node": {
"version": "18.6.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz",
"integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg=="
},
"node_modules/@certusone/wormhole-spydk": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-spydk/-/wormhole-spydk-0.0.1.tgz",
@ -1505,9 +1557,9 @@
}
},
"node_modules/@improbable-eng/grpc-web": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.14.1.tgz",
"integrity": "sha512-XaIYuunepPxoiGVLLHmlnVminUGzBTnXr8Wv7khzmLWbNw4TCwJKX09GSMJlKhu/TRk6gms0ySFxewaETSBqgw==",
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz",
"integrity": "sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg==",
"dependencies": {
"browser-headers": "^0.4.1"
},
@ -1973,13 +2025,25 @@
"ieee754": "^1.2.1"
}
},
"node_modules/@terra-money/terra.js": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-3.0.8.tgz",
"integrity": "sha512-TSosUWw1OeZmgliHwgydDBgEEl+dGnAoFeaYmYYv+dzcYFnyUwY4NXpvg2cU0rjPBLGQHQdV/zRRfSyNQlGDBQ==",
"node_modules/@terra-money/legacy.proto": {
"name": "@terra-money/terra.proto",
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz",
"integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==",
"dependencies": {
"@terra-money/terra.proto": "^0.1.7",
"axios": "^0.24.0",
"google-protobuf": "^3.17.3",
"long": "^4.0.0",
"protobufjs": "~6.11.2"
}
},
"node_modules/@terra-money/terra.js": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-3.1.4.tgz",
"integrity": "sha512-8kyi9guBHQiTR0d5VXqC2jdNzHHSx0N6NjYUmRCpuoJMn6famaTYlO0+IUK6C9atsk+gNY0l+EIVYYFOZKbagA==",
"dependencies": {
"@terra-money/legacy.proto": "npm:@terra-money/terra.proto@^0.1.7",
"@terra-money/terra.proto": "~2.0.0",
"axios": "^0.26.1",
"bech32": "^2.0.0",
"bip32": "^2.0.6",
"bip39": "^3.0.3",
@ -1996,16 +2060,36 @@
"node": ">=14"
}
},
"node_modules/@terra-money/terra.proto": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz",
"integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==",
"node_modules/@terra-money/terra.js/node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dependencies": {
"follow-redirects": "^1.14.8"
}
},
"node_modules/@terra-money/terra.proto": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-2.0.0.tgz",
"integrity": "sha512-ZjyFOFUzrGn8IwzGIgr1OJFcPSsQoz/XAfoSKThJx+OjJA7CLhdcz51+5h7ehNfb+qB9wr7aNME0h24wu9D4SQ==",
"dependencies": {
"@improbable-eng/grpc-web": "^0.14.1",
"google-protobuf": "^3.17.3",
"long": "^4.0.0",
"protobufjs": "~6.11.2"
}
},
"node_modules/@terra-money/terra.proto/node_modules/@improbable-eng/grpc-web": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.14.1.tgz",
"integrity": "sha512-XaIYuunepPxoiGVLLHmlnVminUGzBTnXr8Wv7khzmLWbNw4TCwJKX09GSMJlKhu/TRk6gms0ySFxewaETSBqgw==",
"dependencies": {
"browser-headers": "^0.4.1"
},
"peerDependencies": {
"google-protobuf": "^3.14.0"
}
},
"node_modules/@terra-money/use-wallet": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@terra-money/use-wallet/-/use-wallet-3.8.0.tgz",
@ -2234,9 +2318,9 @@
"integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw=="
},
"node_modules/@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
},
"node_modules/@types/mime": {
"version": "1.3.2",
@ -4084,9 +4168,9 @@
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"node_modules/follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"funding": [
{
"type": "individual",
@ -6554,9 +6638,9 @@
}
},
"node_modules/rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
"integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
"dependencies": {
"tslib": "^2.1.0"
}
@ -8271,20 +8355,72 @@
"requires": {}
},
"@certusone/wormhole-sdk": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.3.8.tgz",
"integrity": "sha512-XHWKRqYo+Euj8DNWPLM3j5cXAqVkp4QtmpYqheLqO3hQLIc5MvyHZfXHxfAq0gmrdQT89xP5Dq8Q3uLGtq07QQ==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.5.0.tgz",
"integrity": "sha512-Z8Cj2yZ41if842jSSLzKLomwkq9PgXdjVq3r3VNzkSM3aZavU8vZqNT33LU9IabmW7hiWe1uI9j2Z1JZe7SIEg==",
"requires": {
"@improbable-eng/grpc-web": "^0.14.0",
"@certusone/wormhole-sdk-proto-web": "^0.0.1",
"@certusone/wormhole-sdk-wasm": "^0.0.1",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^3.0.7",
"@terra-money/terra.js": "^3.1.3",
"algosdk": "^1.15.0",
"axios": "^0.24.0",
"bech32": "^2.0.0",
"js-base64": "^3.6.1",
"protobufjs": "^6.11.2",
"rxjs": "^7.3.0"
"js-base64": "^3.6.1"
}
},
"@certusone/wormhole-sdk-proto-web": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-proto-web/-/wormhole-sdk-proto-web-0.0.1.tgz",
"integrity": "sha512-v6D+vCPqzTmrRuN0ZHpOdA1XnF3nmaD1wlJf025SXb7JFhVSmKyFXzLajkt50rk6SCkEvXtRlxNTJtnuCxg94Q==",
"requires": {
"@improbable-eng/grpc-web": "^0.15.0",
"protobufjs": "^7.0.0",
"rxjs": "^7.5.6"
},
"dependencies": {
"long": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
},
"protobufjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.0.0.tgz",
"integrity": "sha512-ffNIEm+quOcYtQvHdW406v1NQmZSuqVklxsXk076BtuFnlYZfigLU+JOMrTD8TUOyqHYbRI/fSVNvgd25YeN3w==",
"requires": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
}
}
}
},
"@certusone/wormhole-sdk-wasm": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-wasm/-/wormhole-sdk-wasm-0.0.1.tgz",
"integrity": "sha512-LdIwLhOyr4pPs2jqYubqC7d4UkqYBX0EG/ppspQlW3qlVE0LZRMrH6oVzzLMyHtV0Rw7O9sIKzORW/T3mrJv2w==",
"requires": {
"@types/long": "^4.0.2",
"@types/node": "^18.0.3"
},
"dependencies": {
"@types/node": {
"version": "18.6.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz",
"integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg=="
}
}
},
"@certusone/wormhole-spydk": {
@ -8730,9 +8866,9 @@
}
},
"@improbable-eng/grpc-web": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.14.1.tgz",
"integrity": "sha512-XaIYuunepPxoiGVLLHmlnVminUGzBTnXr8Wv7khzmLWbNw4TCwJKX09GSMJlKhu/TRk6gms0ySFxewaETSBqgw==",
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz",
"integrity": "sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg==",
"requires": {
"browser-headers": "^0.4.1"
}
@ -9108,13 +9244,24 @@
}
}
},
"@terra-money/terra.js": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-3.0.8.tgz",
"integrity": "sha512-TSosUWw1OeZmgliHwgydDBgEEl+dGnAoFeaYmYYv+dzcYFnyUwY4NXpvg2cU0rjPBLGQHQdV/zRRfSyNQlGDBQ==",
"@terra-money/legacy.proto": {
"version": "npm:@terra-money/terra.proto@0.1.7",
"resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz",
"integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==",
"requires": {
"@terra-money/terra.proto": "^0.1.7",
"axios": "^0.24.0",
"google-protobuf": "^3.17.3",
"long": "^4.0.0",
"protobufjs": "~6.11.2"
}
},
"@terra-money/terra.js": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-3.1.4.tgz",
"integrity": "sha512-8kyi9guBHQiTR0d5VXqC2jdNzHHSx0N6NjYUmRCpuoJMn6famaTYlO0+IUK6C9atsk+gNY0l+EIVYYFOZKbagA==",
"requires": {
"@terra-money/legacy.proto": "npm:@terra-money/terra.proto@^0.1.7",
"@terra-money/terra.proto": "~2.0.0",
"axios": "^0.26.1",
"bech32": "^2.0.0",
"bip32": "^2.0.6",
"bip39": "^3.0.3",
@ -9126,16 +9273,37 @@
"tmp": "^0.2.1",
"utf-8-validate": "^5.0.5",
"ws": "^7.5.5"
},
"dependencies": {
"axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"requires": {
"follow-redirects": "^1.14.8"
}
}
}
},
"@terra-money/terra.proto": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz",
"integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-2.0.0.tgz",
"integrity": "sha512-ZjyFOFUzrGn8IwzGIgr1OJFcPSsQoz/XAfoSKThJx+OjJA7CLhdcz51+5h7ehNfb+qB9wr7aNME0h24wu9D4SQ==",
"requires": {
"@improbable-eng/grpc-web": "^0.14.1",
"google-protobuf": "^3.17.3",
"long": "^4.0.0",
"protobufjs": "~6.11.2"
},
"dependencies": {
"@improbable-eng/grpc-web": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.14.1.tgz",
"integrity": "sha512-XaIYuunepPxoiGVLLHmlnVminUGzBTnXr8Wv7khzmLWbNw4TCwJKX09GSMJlKhu/TRk6gms0ySFxewaETSBqgw==",
"requires": {
"browser-headers": "^0.4.1"
}
}
}
},
"@terra-money/use-wallet": {
@ -9333,9 +9501,9 @@
"integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw=="
},
"@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
},
"@types/mime": {
"version": "1.3.2",
@ -10848,9 +11016,9 @@
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
},
"form-data": {
"version": "3.0.1",
@ -12728,9 +12896,9 @@
}
},
"rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
"integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
"requires": {
"tslib": "^2.1.0"
}

View File

@ -33,7 +33,7 @@
},
"dependencies": {
"@celo-tools/celo-ethers-wrapper": "^0.1.0",
"@certusone/wormhole-sdk": "^0.3.8",
"@certusone/wormhole-sdk": "^0.5.0",
"@certusone/wormhole-spydk": "^0.0.1",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",

View File

@ -34,7 +34,7 @@ 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:4200";
export const SPY_RELAY_URL = "http://localhost:4201";
describe("consts should exist", () => {
it("has Solana test token", () => {

View File

@ -19,10 +19,8 @@ import {
postVaaSolana,
parseSequenceFromLogEth,
parseSequenceFromLogSolana,
redeemOnSolana,
transferFromEth,
transferFromSolana,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import getSignedVAAWithRetry from "@certusone/wormhole-sdk/lib/cjs/rpc/getSignedVAAWithRetry";
@ -46,7 +44,6 @@ import {
ETH_CORE_BRIDGE_ADDRESS,
ETH_NODE_URL,
ETH_PRIVATE_KEY,
ETH_PUBLIC_KEY,
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_CORE_BRIDGE_ADDRESS,
SOLANA_HOST,
@ -91,9 +88,9 @@ test("Verify Spy Relay is running", (done) => {
})();
});
var sequence: string;
var emitterAddress: string;
var transferSignedVAA: Uint8Array;
let sequence: string;
let emitterAddress: string;
let transferSignedVAA: Uint8Array;
describe("Solana to Ethereum", () => {
test("Attest Solana SPL to Ethereum", (done) => {
@ -157,7 +154,6 @@ describe("Solana to Ethereum", () => {
}
})();
});
// TODO: it is attested
test("Send Solana SPL to Ethereum", (done) => {
(async () => {
@ -182,6 +178,7 @@ describe("Solana to Ethereum", () => {
// 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,
@ -189,9 +186,13 @@ describe("Solana to Ethereum", () => {
payerAddress,
fromAddress,
TEST_SOLANA_TOKEN,
amount,
amount + fee,
hexToUint8Array(nativeToHexString(targetAddress, CHAIN_ID_ETH) || ""),
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.");
@ -234,13 +235,11 @@ describe("Solana to Ethereum", () => {
}
})();
});
test("Spy Relay redeemed on Eth", (done) => {
(async () => {
try {
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
var success: boolean = false;
let success: boolean = false;
for (let count = 0; count < 5 && !success; ++count) {
console.log(
"sleeping before querying spy relay",
@ -258,9 +257,7 @@ describe("Solana to Ethereum", () => {
count
);
}
expect(success).toBe(true);
provider.destroy();
done();
} catch (e) {
@ -269,43 +266,6 @@ describe("Solana to Ethereum", () => {
}
})();
});
test("Query Spy Relay via REST", (done) => {
(async () => {
var storeKey: string =
CHAIN_ID_SOLANA.toString() +
"/" +
emitterAddress +
"/" +
sequence.toString();
try {
var query: string = SPY_RELAY_URL + "/query/" + storeKey;
console.log("Sending query to spy relay, query: [%s]", query);
const result = await axios.get(query);
console.log(
"status: ",
result.status,
", statusText: ",
result.statusText,
", data: ",
result.data
);
expect(result).toHaveProperty("status");
expect(result.status).toBe(200);
expect(result).toHaveProperty("data");
expect(JSON.parse(result.data).vaa_bytes).toBe(
uint8ArrayToHex(transferSignedVAA)
);
console.log(result.data);
done();
} catch (e) {
console.error(e);
done("An error occurred while trying to send query to spy relay");
}
})();
});
});
describe("Ethereum to Solana", () => {
@ -387,7 +347,6 @@ describe("Ethereum to Solana", () => {
// create a keypair for Solana
const connection = new Connection(SOLANA_HOST, "confirmed");
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
const payerAddress = keypair.publicKey.toString();
// determine destination address - an associated token account
const solanaMintKey = new PublicKey(
(await getForeignAssetSolana(
@ -432,18 +391,26 @@ describe("Ethereum to Solana", () => {
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, amount);
await approveEth(
ETH_TOKEN_BRIDGE_ADDRESS,
TEST_ERC20,
signer,
transferAmount
);
// transfer tokens
const receipt = await transferFromEth(
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
TEST_ERC20,
amount,
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);
@ -460,17 +427,6 @@ describe("Ethereum to Solana", () => {
);
console.log("Got signed vaa: ", signedVAA);
transferSignedVAA = signedVAA;
// post vaa to Solana
// await postVaaSolana( // I think this is the redeem!
// connection,
// async (transaction) => {
// transaction.partialSign(keypair);
// return transaction;
// },
// SOLANA_CORE_BRIDGE_ADDRESS,
// payerAddress,
// Buffer.from(signedVAA)
// );
provider.destroy();
done();
} catch (e) {
@ -479,13 +435,11 @@ describe("Ethereum to Solana", () => {
}
})();
});
test("Spy Relay redeemed on Sol", (done) => {
(async () => {
try {
const connection = new Connection(SOLANA_HOST, "confirmed");
var success: boolean = false;
let success: boolean = false;
for (let count = 0; count < 5 && !success; ++count) {
console.log(
"sleeping before querying spy relay",
@ -503,7 +457,7 @@ describe("Ethereum to Solana", () => {
count
);
}
expect(success).toBe(true);
done();
} catch (e) {
console.error(e);
@ -511,43 +465,6 @@ describe("Ethereum to Solana", () => {
}
})();
});
test("Query Spy Relay via REST", (done) => {
(async () => {
var storeKey: string =
CHAIN_ID_ETH.toString() +
"/" +
emitterAddress +
"/" +
sequence.toString();
try {
var query: string = SPY_RELAY_URL + "/query/" + storeKey;
console.log("Sending query to spy relay, query: [%s]", query);
const result = await axios.get(query);
console.log(
"status: ",
result.status,
", statusText: ",
result.statusText,
", data: ",
result.data
);
expect(result).toHaveProperty("status");
expect(result.status).toBe(200);
expect(result).toHaveProperty("data");
expect(JSON.parse(result.data).vaa_bytes).toBe(
uint8ArrayToHex(transferSignedVAA)
);
console.log(result.data);
done();
} catch (e) {
console.error(e);
done("An error occurred while trying to send query to spy relay");
}
})();
});
});
describe("Ethereum to Terra", () => {
@ -582,6 +499,7 @@ describe("Ethereum to Terra", () => {
const lcd = new LCDClient({
URL: TERRA_NODE_URL,
chainID: TERRA_CHAIN_ID,
isClassic: true,
});
const mk = new MnemonicKey({
mnemonic: TERRA_PRIVATE_KEY,
@ -633,11 +551,19 @@ describe("Ethereum to Terra", () => {
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, amount);
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,
@ -648,11 +574,12 @@ describe("Ethereum to Terra", () => {
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
TEST_ERC20,
amount,
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);
@ -669,49 +596,6 @@ describe("Ethereum to Terra", () => {
);
console.log("Got signed vaa: ", signedVAA);
transferSignedVAA = signedVAA;
// expect(
// await getIsTransferCompletedTerra(
// TERRA_TOKEN_BRIDGE_ADDRESS,
// signedVAA,
// wallet.key.accAddress,
// lcd,
// TERRA_GAS_PRICES_URL
// )
// ).toBe(false);
// const msg = await redeemOnTerra(
// TERRA_TOKEN_BRIDGE_ADDRESS,
// wallet.key.accAddress,
// signedVAA
// );
// const gasPrices = await axios
// .get(TERRA_GAS_PRICES_URL)
// .then((result) => result.data);
// const feeEstimate = await lcd.tx.estimateFee(
// wallet.key.accAddress,
// [msg],
// {
// memo: "localhost",
// feeDenoms: ["uluna"],
// gasPrices,
// }
// );
// const tx = await wallet.createAndSignTx({
// msgs: [msg],
// memo: "localhost",
// feeDenoms: ["uluna"],
// gasPrices,
// fee: feeEstimate,
// });
// await lcd.tx.broadcast(tx);
// expect(
// await getIsTransferCompletedTerra(
// TERRA_TOKEN_BRIDGE_ADDRESS,
// signedVAA,
// wallet.key.accAddress,
// lcd,
// TERRA_GAS_PRICES_URL
// )
// ).toBe(true);
provider.destroy();
done();
} catch (e) {
@ -727,12 +611,8 @@ describe("Ethereum to Terra", () => {
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);
var success: boolean = false;
for (let count = 0; count < 5 && !success; ++count) {
console.log(
@ -752,7 +632,7 @@ describe("Ethereum to Terra", () => {
count
);
}
expect(success).toBe(true);
done();
} catch (e) {
console.error(e);
@ -762,38 +642,4 @@ describe("Ethereum to Terra", () => {
}
})();
});
test("Query Spy Relay via REST", (done) => {
(async () => {
var storeKey: string =
CHAIN_ID_TERRA.toString() +
"/" +
emitterAddress +
"/" +
sequence.toString();
try {
var query: string = SPY_RELAY_URL + "/query/" + storeKey;
console.log("Sending query to spy relay, query: [%s]", query);
const result = await axios.get(query);
console.log(
"status: ",
result.status,
", statusText: ",
result.statusText,
", data: ",
result.data
);
expect(result).toHaveProperty("status");
expect(result.status).toBe(200);
expect(result).toHaveProperty("data");
console.log(result.data);
done();
} catch (e) {
console.error(e);
done("An error occurred while trying to send query to spy relay");
}
})();
});
});

View File

@ -0,0 +1,11 @@
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;

View File

@ -0,0 +1,291 @@
/** The default backend is relaying payload 1 token bridge messages only */
import {
ChainId,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
uint8ArrayToHex,
tryHexToNativeString,
getEmitterAddressEth,
getEmitterAddressSolana,
getEmitterAddressTerra,
parseTransferPayload,
} 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 (myChainId === CHAIN_ID_TERRA) {
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 originChain = parsedVaa.payload.originChain;
const originAddress = parsedVaa.payload.originAddress;
let originAddressNative: string;
try {
originAddressNative = tryHexToNativeString(originAddress, originChain);
} catch (e: any) {
this.logger.error(
`Failure to convert address "${originAddress}" on chain "${originChain}" to the native address`
);
return;
}
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);
}
}

View File

@ -0,0 +1,393 @@
import {
ChainId,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
tryHexToNativeString,
hexToUint8Array,
importCoreWasm,
isEVMChain,
parseTransferPayload,
CHAIN_ID_UNSET,
} 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 (transferPayload.targetChain === CHAIN_ID_TERRA) {
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" };
}
}

View File

@ -0,0 +1,61 @@
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;
}

View File

@ -0,0 +1,24 @@
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;
};

View File

@ -4,7 +4,7 @@
"chainName": "Solana",
"nativeCurrencySymbol": "SOL",
"nodeUrl": "http://solana-devnet:8899",
"tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16",
"tokenBridgeAddress": "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE",
"bridgeAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o",
"wrappedAsset": "So11111111111111111111111111111111111111112"
},
@ -23,9 +23,10 @@
"nodeUrl": "http://terra-terrad:1317",
"tokenBridgeAddress": "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4",
"terraName": "localterra",
"terraChainId": "columbus-5",
"terraChainId": "localterra",
"terraCoin": "uluna",
"terraGasPriceUrl": "http://terra-fcd:3060/v1/txs/gas_prices"
"terraGasPriceUrl": "http://terra-fcd:3060/v1/txs/gas_prices",
"isTerraClassic": true
},
{
"chainId": 4,

View File

@ -102,6 +102,7 @@ export type ChainConfigInfo = {
terraCoin?: string;
terraGasPriceUrl?: string;
wrappedAsset?: string | null;
isTerraClassic?: boolean;
};
export type ListenerEnvironment = {
@ -202,6 +203,8 @@ const createListenerEnvironment: () => ListenerEnvironment = () => {
}
}
logger.info("Setting the listener backend...");
return {
spyServiceHost,
spyServiceFilters,
@ -230,6 +233,7 @@ const createRelayerEnvironment: () => RelayerEnvironment = () => {
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");
@ -290,6 +294,8 @@ const createRelayerEnvironment: () => RelayerEnvironment = () => {
}
}
logger.info("Setting the relayer backend...");
return {
supportedChains,
redisHost,
@ -442,6 +448,7 @@ function createTerraChainConfig(
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");
@ -488,6 +495,7 @@ function createTerraChainConfig(
terraChainId = config.terraChainId;
terraCoin = config.terraCoin;
terraGasPriceUrl = config.terraGasPriceUrl;
isTerraClassic = config.isTerraClassic || false;
return {
chainId,
@ -500,6 +508,7 @@ function createTerraChainConfig(
terraChainId,
terraCoin,
terraGasPriceUrl,
isTerraClassic,
};
}

View File

@ -156,10 +156,6 @@ export async function addToRedis(
}
}
export function getKey(chainId: ChainId, address: string) {
return chainId + ":" + address;
}
export enum Status {
Pending = 1,
Completed = 2,
@ -174,6 +170,7 @@ export type RelayResult = {
export type WorkerInfo = {
index: number;
targetChainName: string;
targetChainId: number;
walletPrivateKey: any;
};
@ -191,6 +188,7 @@ export type StorePayload = {
retries: number;
};
/** Default redis payload */
export function initPayload(): StorePayload {
return {
vaa_bytes: "",
@ -199,6 +197,7 @@ export function initPayload(): StorePayload {
retries: 0,
};
}
export function initPayloadWithVAA(vaa_bytes: string): StorePayload {
const sp: StorePayload = initPayload();
sp.vaa_bytes = vaa_bytes;
@ -215,6 +214,7 @@ export function storeKeyFromParsedVAA(
};
}
/** Stringify the key going into redis as json */
export function storeKeyToJson(storeKey: StoreKey): string {
return JSON.stringify(storeKey);
}
@ -223,6 +223,7 @@ 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);
}
@ -235,54 +236,6 @@ export function resetPayload(storePayload: StorePayload): StorePayload {
return initPayloadWithVAA(storePayload.vaa_bytes);
}
export async function pushVaaToRedis(
parsedVAA: ParsedVaa<ParsedTransferPayload>,
hexVaa: string
) {
const transferPayload = parsedVAA.payload;
logger.info(
"forwarding vaa to relayer: emitter: [" +
parsedVAA.emitterChain +
":" +
uint8ArrayToHex(parsedVAA.emitterAddress) +
"], seqNum: " +
parsedVAA.sequence +
", payload: origin: [" +
transferPayload.originAddress +
":" +
transferPayload.originAddress +
"], target: [" +
transferPayload.targetChain +
":" +
transferPayload.targetAddress +
"], amount: " +
transferPayload.amount +
"], fee: " +
transferPayload.fee +
", "
);
const storeKey = storeKeyFromParsedVAA(parsedVAA);
const storePayload = initPayloadWithVAA(hexVaa);
logger.debug(
"storing: key: [" +
storeKey.chain_id +
"/" +
storeKey.emitter_address +
"/" +
storeKey.sequence +
"], payload: [" +
storePayloadToJson(storePayload) +
"]"
);
await storeInRedis(
storeKeyToJson(storeKey),
storePayloadToJson(storePayload)
);
}
export async function clearRedis() {
const redisClient = await connectToRedis();
if (!redisClient) {
@ -428,3 +381,44 @@ export async function monitorRedis(metrics: PromHelper) {
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;
}

View File

@ -1,27 +1,12 @@
import { uint8ArrayToHex } from "@certusone/wormhole-sdk";
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
import { Request, Response } from "express";
import { getBackend } from "../backends";
import { getListenerEnvironment, ListenerEnvironment } from "../configureEnv";
import { getLogger } from "../helpers/logHelper";
import {
initPayloadWithVAA,
pushVaaToRedis,
storeInRedis,
storeKeyFromParsedVAA,
storeKeyToJson,
storePayloadToJson,
} from "../helpers/redisHelper";
import {
parseAndValidateVaa,
ParsedTransferPayload,
ParsedVaa,
} from "./validation";
let logger = getLogger();
let env: ListenerEnvironment;
export function init(runRest: boolean): boolean {
if (!runRest) return true;
export function init(): boolean {
try {
env = getListenerEnvironment();
} catch (e) {
@ -51,17 +36,8 @@ export async function run() {
(async () => {
app.get("/relayvaa/:vaa", async (req: Request, res: Response) => {
try {
const vaaBuf = Uint8Array.from(Buffer.from(req.params.vaa, "base64"));
const hexVaa = uint8ArrayToHex(vaaBuf);
const validationResults: ParsedVaa<ParsedTransferPayload> | string =
await parseAndValidateVaa(vaaBuf);
if (typeof validationResults === "string") {
logger.debug("Rejecting REST request due validation failure");
return;
}
pushVaaToRedis(validationResults, hexVaa);
const rawVaa = Uint8Array.from(Buffer.from(req.params.vaa, "base64"));
await getBackend().listener.process(rawVaa);
res.status(200).json({ message: "Scheduled" });
} catch (e) {

View File

@ -1,47 +1,24 @@
import {
ChainId,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
getEmitterAddressEth,
getEmitterAddressSolana,
getEmitterAddressTerra,
hexToUint8Array,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
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 {
initPayloadWithVAA,
pushVaaToRedis,
storeInRedis,
storeKeyFromParsedVAA,
storeKeyToJson,
storePayloadToJson,
} from "../helpers/redisHelper";
import { sleep } from "../helpers/utils";
import {
parseAndValidateVaa,
ParsedTransferPayload,
ParsedVaa,
} from "./validation";
let metrics: PromHelper;
let env: ListenerEnvironment;
let logger = getLogger();
let vaaUriPrelude: string;
export function init(runListen: boolean): boolean {
if (!runListen) return true;
export function init(): boolean {
try {
env = getListenerEnvironment();
vaaUriPrelude =
"http://localhost:" +
(process.env.REST_PORT ? process.env.REST_PORT : "4200") +
(process.env.REST_PORT ? process.env.REST_PORT : "4201") +
"/relayvaa/";
} catch (e) {
logger.error("Error initializing listener environment: " + e);
@ -55,65 +32,26 @@ export async function run(ph: PromHelper) {
const logger = getLogger();
metrics = ph;
logger.info("Attempting to run Listener...");
let typedFilters: {
emitterFilter: { chainId: ChainId; emitterAddress: string };
}[] = [];
for (let i = 0; i < env.spyServiceFilters.length; i++) {
logger.info("Getting spyServiceFiltera " + i);
const filter = env.spyServiceFilters[i];
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
),
},
};
logger.info("Getting spyServiceFilterc " + i);
logger.info(
"adding filter: chainId: [" +
typedFilter.emitterFilter.chainId +
"], emitterAddress: [" +
typedFilter.emitterFilter.emitterAddress +
"]"
);
logger.info("Getting spyServiceFilterd " + i);
typedFilters.push(typedFilter);
logger.info("Getting spyServiceFiltere " + i);
}
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 {
//TODO use ENV object
const client = createSpyRPCServiceClient(
process.env.SPY_SERVICE_HOST || ""
);
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);
processVaa(asUint8);
getBackend().listener.process(asUint8);
});
let connected = true;
@ -138,43 +76,8 @@ export async function run(ph: PromHelper) {
logger.error("spy service threw an exception: %o", e);
}
stream.end;
stream.destroy()
await sleep(5 * 1000);
logger.info("attempting to reconnect to the spy service");
}
}
async function processVaa(rawVaa: Uint8Array) {
//TODO, verify this is correct & potentially swap to using hex encoding
const vaaUri =
vaaUriPrelude + encodeURIComponent(Buffer.from(rawVaa).toString("base64"));
const validationResults: ParsedVaa<ParsedTransferPayload> | string =
await parseAndValidateVaa(rawVaa);
metrics.incIncoming();
if (typeof validationResults === "string") {
logger.debug("Rejecting spied request due validation failure");
return;
}
const parsedVAA: ParsedVaa<ParsedTransferPayload> = validationResults;
await pushVaaToRedis(parsedVAA, uint8ArrayToHex(rawVaa));
}
async function encodeEmitterAddress(
myChainId: ChainId,
emitterAddressStr: string
): Promise<string> {
if (myChainId === CHAIN_ID_SOLANA) {
return await getEmitterAddressSolana(emitterAddressStr);
}
if (myChainId === CHAIN_ID_TERRA) {
return await getEmitterAddressTerra(emitterAddressStr);
}
return getEmitterAddressEth(emitterAddressStr);
}

View File

@ -1,207 +1,5 @@
import {
ChainId,
hexToNativeString,
parseTransferPayload,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import { ChainId } from "@certusone/wormhole-sdk";
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
import { getListenerEnvironment } from "../configureEnv";
import { getLogger } from "../helpers/logHelper";
import {
connectToRedis,
getBackupQueue,
getKey,
RedisTables,
} from "../helpers/redisHelper";
const logger = getLogger();
export function validateInit(): boolean {
const env = getListenerEnvironment();
logger.info(
"supported target chains: [" + env.spyServiceFilters.toString() + "]"
);
if (env.spyServiceFilters.length) {
env.spyServiceFilters.forEach((allowedContract) => {
logger.info(
"adding allowed contract: chainId: [" +
allowedContract.chainId +
"] => address: [" +
allowedContract.emitterAddress +
"]"
);
});
} else {
logger.info("There are no white listed contracts provisioned.");
}
logger.info("supported tokens : [" + env.supportedTokens.toString() + "]");
if (env.supportedTokens.length) {
env.supportedTokens.forEach((supportedToken) => {
logger.info(
"adding allowed contract: chainId: [" +
supportedToken.chainId +
"] => address: [" +
supportedToken.address +
"]" +
" key: " +
getKey(supportedToken.chainId, supportedToken.address)
);
});
} else {
logger.info("There are no white listed contracts provisioned.");
}
return true;
}
export async function parseAndValidateVaa(
rawVaa: Uint8Array
): Promise<string | ParsedVaa<ParsedTransferPayload>> {
logger.debug("About to validate: " + uint8ArrayToHex(rawVaa));
let parsedVaa: ParsedVaa<Uint8Array> | null = null;
try {
parsedVaa = await parseVaaTyped(rawVaa);
} catch (e) {
logger.error("Encountered error while parsing raw VAA " + e);
}
if (!parsedVaa) {
return "Unable to parse the specified VAA.";
}
const env = getListenerEnvironment();
//You have to derive all the emitter addresses from the native addresses, because emitter addresses cannot be mapped backwards to native.
//This is especially important because they are only uninvertible on Solana, and if you convert the emitter addresses to native,
//It will work for all chains except Solana.
//TODO calc emitter addresses, and compare against those, rather than getting the natives from the emitter
// const nativeAddress = hexToNativeString(
// uint8ArrayToHex(parsedVaa.emitterAddress),
// parsedVaa.emitterChain
// );
// logger.info("nativeAddress format for emitter address in validator:" + nativeAddress);
// const isApprovedAddress = env.spyServiceFilters.find((allowedContract) => {
// console.log(
// parsedVaa,
// nativeAddress,
// allowedContract.emitterAddress,
// "in approved address"
// );
// return (
// parsedVaa &&
// nativeAddress &&
// allowedContract.chainId === parsedVaa.emitterChain &&
// allowedContract.emitterAddress.toLowerCase() ===
// nativeAddress.toLowerCase()
// );
// });
// if (!isApprovedAddress) {
// logger.debug("Specified vaa is not from an approved address.");
// return "VAA is not from a monitored contract.";
// }
const isCorrectPayloadType = parsedVaa.payload[0] === 1;
if (!isCorrectPayloadType) {
logger.debug("Specified vaa is not payload type 1.");
return "Specified vaa is not payload type 1..";
}
let parsedPayload: any = null;
try {
parsedPayload = parseTransferPayload(Buffer.from(parsedVaa.payload));
} catch (e) {
logger.error("Encountered error while parsing vaa payload" + e);
}
if (!parsedPayload) {
logger.debug("Failed to parse the transfer payload.");
return "Could not parse the transfer payload.";
}
const originAddressNative = hexToNativeString(
parsedPayload.originAddress,
parsedPayload.originChain
);
const isApprovedToken = env.supportedTokens.find((token) => {
return (
originAddressNative &&
token.address.toLowerCase() === originAddressNative.toLowerCase() &&
token.chainId === parsedPayload.originChain
);
});
if (!isApprovedToken) {
logger.debug("Token transfer is not for an approved token.");
return "Token transfer is not for an approved token.";
}
//TODO configurable
const sufficientFee = parsedPayload.fee && parsedPayload.fee > 0;
if (!sufficientFee) {
logger.debug("Token transfer does not have a sufficient fee.");
return "Token transfer does not have a sufficient fee.";
}
const key = getKey(parsedPayload.originChain, originAddressNative as string); //was null checked above
const isQueued = await checkQueue(key);
if (isQueued) {
return isQueued;
}
//TODO maybe an is redeemed check?
const fullyTyped = { ...parsedVaa, payload: parsedPayload };
return fullyTyped;
}
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;
}
//TODO move these to the official SDK
export async function parseVaaTyped(signedVAA: Uint8Array) {
@ -230,9 +28,22 @@ export type ParsedVaa<T> = {
export type ParsedTransferPayload = {
amount: BigInt;
originAddress: Uint8Array; //hex
originAddress: string; // hex
originChain: ChainId;
targetAddress: Uint8Array; //hex
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"
);
}

View File

@ -22,7 +22,7 @@ const logger = getLogger();
// Load the relay config data.
let runListen: boolean = true;
let runWorker: boolean = true;
let runRelayWorker: boolean = true;
let runRest: boolean = true;
let runWalletMonitor: boolean = true;
let foundOne: boolean = false;
@ -37,7 +37,7 @@ for (let idx = 0; idx < process.argv.length; ++idx) {
}
logger.info("spy_relay is running in listen only mode");
runWorker = false;
runRelayWorker = false;
runWalletMonitor = false;
foundOne = true;
}
@ -66,7 +66,7 @@ for (let idx = 0; idx < process.argv.length; ++idx) {
logger.info("spy_relay is running in wallet monitor only mode");
runListen = false;
runRest = false;
runWorker = false;
runRelayWorker = false;
foundOne = true;
}
}
@ -75,49 +75,60 @@ if (!foundOne) {
logger.info("spy_relay is running both the listener and relayer");
}
if (
!error &&
spyListener.init(runListen) &&
relayWorker.init(runWorker) &&
restListener.init(runRest) &&
walletMonitor.init(runWalletMonitor)
) {
const commonEnv = getCommonEnvironment();
const { promPort, readinessPort } = commonEnv;
logger.info("prometheus client listening on port " + promPort);
let promClient: PromHelper;
const runAll: boolean = runListen && runWorker && runWalletMonitor;
if (runAll) {
promClient = new PromHelper("spy_relay", promPort, PromMode.All);
} else if (runListen) {
promClient = new PromHelper("spy_relay", promPort, PromMode.Listen);
} else if (runWorker) {
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 (runWorker) 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");
});
}
} else {
logger.error("Initialization failed.");
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");
});
}

View File

@ -8,9 +8,7 @@ let metrics: PromHelper;
const logger = getLogger();
let relayerEnv: RelayerEnvironment;
export function init(runWorker: boolean): boolean {
if (!runWorker) return true;
export function init(): boolean {
try {
relayerEnv = getRelayerEnvironment();
} catch (e) {

View File

@ -70,6 +70,7 @@ test("should pull Terra token balances", async () => {
URL: terraChainConfig.nodeUrl,
chainID: terraChainConfig.terraChainId,
name: terraChainConfig.terraName,
isClassic: true,
};
const lcd = new LCDClient(lcdConfig);
const localAddresses = await calcLocalAddressesTerra(

View File

@ -45,15 +45,8 @@ export interface TerraNativeBalances {
[index: string]: string;
}
function init() {
try {
env = getRelayerEnvironment();
} catch (e) {
logger.error("Unable to instantiate the relayerEnv in wallet monitor");
}
}
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");
@ -318,7 +311,6 @@ export async function collectWallets(metrics: PromHelper) {
const scopedLogger = getScopedLogger(["collectWallets"], logger);
const ONE_MINUTE: number = 60000;
scopedLogger.info("Starting up.");
init();
while (true) {
scopedLogger.debug("Pulling balances.");
let wallets: WalletBalance[] = [];
@ -507,6 +499,7 @@ async function pullAllTerraBalances(
URL: chainConfig.nodeUrl,
chainID: chainConfig.terraChainId,
name: chainConfig.terraName,
isClassic: chainConfig.isTerraClassic,
};
const lcd = new LCDClient(lcdConfig);
const localAddresses = await calcLocalAddressesTerra(

View File

@ -1,134 +0,0 @@
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
import {
ChainId,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
hexToNativeString,
hexToUint8Array,
isEVMChain,
parseTransferPayload,
} from "@certusone/wormhole-sdk";
import { relayEVM } from "./evm";
import { relaySolana } from "./solana";
import { relayTerra } from "./terra";
import { getRelayerEnvironment } from "../configureEnv";
import { RelayResult, Status } from "../helpers/redisHelper";
import { getLogger, getScopedLogger, ScopedLogger } from "../helpers/logHelper";
import { PromHelper } from "../helpers/promHelpers";
const logger = getLogger();
function getChainConfigInfo(chainId: ChainId) {
const env = getRelayerEnvironment();
return env.supportedChains.find((x) => x.chainId === chainId);
}
export async function 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)) {
const unwrapNative =
transferPayload.originChain === transferPayload.targetChain &&
hexToNativeString(
transferPayload.originAddress,
transferPayload.originChain
)?.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 (transferPayload.targetChain === CHAIN_ID_TERRA) {
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" };
}

View File

@ -9,8 +9,6 @@ import {
demoteWorkingRedis,
monitorRedis,
RedisTables,
RelayResult,
resetPayload,
Status,
StorePayload,
storePayloadFromJson,
@ -18,13 +16,11 @@ import {
WorkerInfo,
} from "../helpers/redisHelper";
import { sleep } from "../helpers/utils";
import { relay } from "./relay";
import { getBackend } from "../backends";
const WORKER_THREAD_RESTART_MS = 10 * 1000;
const AUDITOR_THREAD_RESTART_MS = 10 * 1000;
const AUDIT_INTERVAL_MS = 30 * 1000;
const WORKER_INTERVAL_MS = 5 * 1000;
const REDIS_RETRY_MS = 10 * 1000;
let metrics: PromHelper;
@ -36,9 +32,7 @@ type WorkableItem = {
value: string;
};
export function init(runWorker: boolean): boolean {
if (!runWorker) return true;
export function init(): boolean {
try {
relayerEnv = getRelayerEnvironment();
} catch (e) {
@ -51,7 +45,8 @@ export function init(runWorker: boolean): boolean {
return true;
}
function createWorkerInfos(metrics: PromHelper) {
/** 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) => {
@ -65,14 +60,17 @@ function createWorkerInfos(metrics: PromHelper) {
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++;
});
@ -81,157 +79,35 @@ function createWorkerInfos(metrics: PromHelper) {
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[" +
workerInfo.index +
"] to handle targetChainId " +
workerInfo.targetChainId
`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 = doAuditorThread(workerInfo).catch(
async (error: Error) => {
const auditorPromise = getBackend()
.relayer.runAuditor(workerInfo, metrics)
.catch(async (error: Error) => {
logger.error(
"Fatal crash on auditor thread: index " +
workerInfo.index +
" chainId " +
workerInfo.targetChainId
`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;
}
//One auditor thread should be spawned per worker. This is perhaps overkill, but auditors
//should not be allowed to block workers, or other auditors.
async function doAuditorThread(workerInfo: WorkerInfo) {
const auditLogger = getScopedLogger([`audit-worker-${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 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());
await sleep(AUDIT_INTERVAL_MS);
} catch (e) {
auditLogger.error("spawnAuditorThread: caught exception: " + e);
}
}
}
export async function run(ph: PromHelper) {
metrics = ph;
@ -255,115 +131,6 @@ export async function run(ph: PromHelper) {
}
}
async function processRequest(
key: string,
myPrivateKey: any,
relayLogger: ScopedLogger
) {
const logger = getScopedLogger(["processRequest"], relayLogger);
try {
logger.debug("Processing request %s...", key);
// Get the entry from the working store
const rClient = await connectToRedis();
if (!rClient) {
logger.error("Failed to connect to Redis in processRequest");
return;
}
await rClient.select(RedisTables.WORKING);
let value: string | null = await rClient.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 relay(
payload.vaa_bytes,
false,
myPrivateKey,
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: "Failure",
};
if (e && e.message) {
relayResult.result = e.message;
}
}
const MAX_RETRIES = 10;
let targetChain: any = 0; // 0 is unspecified, but not covered by the SDK
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 rClient.set(key, value);
} else {
// Remove from the working table
await rClient.del(key);
// Put this back into the incoming table
await rClient.select(RedisTables.INCOMING);
await rClient.set(key, value);
}
await rClient.quit();
} catch (e: any) {
logger.error("Unexpected error in processRequest: " + e.message);
logger.error("request key: " + key);
logger.error(e);
return [];
}
}
// 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.
@ -389,8 +156,7 @@ async function findWorkableItems(
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 tgtChainId = transferPayload.targetChain;
const tgtChainId = getBackend().relayer.targetChainId(payloadBuffer);
if (tgtChainId !== workerInfo.targetChainId) {
// Skipping mismatched chainId
continue;
@ -428,13 +194,14 @@ async function findWorkableItems(
}
}
//One worker should be spawned for each chainId+privateKey combo.
/** Spin up one worker for each (chainId, privateKey) combo. */
async function spawnWorkerThread(workerInfo: WorkerInfo) {
logger.info(
"Spinning up worker[" +
workerInfo.index +
"] to handle targetChainId " +
workerInfo.targetChainId
"] to handle target chain " +
workerInfo.targetChainId +
` / ${workerInfo.targetChainName}`
);
const workerPromise = doWorkerThread(workerInfo).catch(async (error) => {
@ -454,7 +221,10 @@ async function spawnWorkerThread(workerInfo: WorkerInfo) {
}
async function doWorkerThread(workerInfo: WorkerInfo) {
const relayLogger = getScopedLogger([`relay-worker-${workerInfo.index}`]);
// 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(
@ -470,10 +240,11 @@ async function doWorkerThread(workerInfo: WorkerInfo) {
relayLogger.debug("Moving item: %o", workItem);
if (await moveToWorking(workItem, relayLogger)) {
relayLogger.info("Moved key to WORKING table: %s", workItem.key);
await processRequest(
await backend.process(
workItem.key,
workerInfo.walletPrivateKey,
relayLogger
relayLogger,
metrics
);
} else {
relayLogger.error(

View File

@ -2,12 +2,12 @@ import {
CHAIN_ID_SOLANA,
getForeignAssetSolana,
getIsTransferCompletedSolana,
hexToNativeString,
hexToUint8Array,
importCoreWasm,
parseTransferPayload,
postVaaSolanaWithRetry,
redeemOnSolana,
tryHexToNativeAssetString,
} from "@certusone/wormhole-sdk";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
@ -72,9 +72,22 @@ export async function relaySolana(
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
? hexToNativeString(transferPayload.originAddress, CHAIN_ID_SOLANA)
? nativeOrigin
: await getForeignAssetSolana(
connection,
chainConfigInfo.tokenBridgeAddress,
@ -83,12 +96,7 @@ export async function relaySolana(
);
if (!solanaMintAddress) {
throw new Error(
`Unable to determine mint for origin chain: ${
transferPayload.originChain
}, address: ${transferPayload.originAddress} (${hexToNativeString(
transferPayload.originAddress,
transferPayload.originChain
)})`
`Unable to determine mint for origin chain: ${transferPayload.originChain}, address: ${transferPayload.originAddress} (${nativeOrigin})`
);
}
const solanaMintKey = new PublicKey(solanaMintAddress);

View File

@ -34,6 +34,7 @@ export async function relayTerra(
URL: chainConfigInfo.nodeUrl,
chainID: chainConfigInfo.terraChainId,
name: chainConfigInfo.terraName,
isClassic: chainConfigInfo.isTerraClassic,
};
const lcd = new LCDClient(lcdConfig);
const mk = new MnemonicKey({

View File

@ -19,5 +19,6 @@ export const chainIDStrings: { [key in ChainId]: string } = {
15: "NEAR",
16: "Moonbeam",
17: "Neon",
18: "Terra2",
10001: "Ropsten",
};

View File

@ -12,7 +12,8 @@
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true,
"isolatedModules": true,
"downlevelIteration": true
"downlevelIteration": true,
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]