From f264f90ada13bb69a258a5ee6e289dfe48fbbf94 Mon Sep 17 00:00:00 2001 From: bigboydiamonds <57741810+bigboydiamonds@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:30:19 -0800 Subject: [PATCH] fix/transaction-polling (#1673) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix/update wagmi main (#1665) * Update wagmi to 1.4.12 * yarn install * Publish - @synapsecns/synapse-interface@0.1.208 * Sdk: expose bridge tx status (#1658) * Add placeholder logic for new functions * Chore: fix spelling * Add tests with expected behavior * Generalize bridgeModule -> RouterSet retrieval * Add skeleton for the end implementation * Implement ID / status for SynapseBridge * CCTP status check * Define behavior for destination CCTP tx * SynapseCCTP: getBridgeID * Add test for incorrect tx hash * Check that transaction receipt is not null * Tests for generalized "incorrect origin tx" behavior * Generalized tool for log extraction * Adapt synapseCCTP, add check to synapseBridge * Chore: bridgeId -> synapseTxId * Extra coverage to keep Code Rabbi happy * Publish - @synapsecns/rest-api@1.0.28 - @synapsecns/sdk-router@0.3.1 - @synapsecns/synapse-interface@0.1.209 * Update polling time * Update polling time * Add constant for polling interval duration --------- Co-authored-by: χ² <88190723+ChiTimesChi@users.noreply.github.com> Co-authored-by: ChiTimesChi Co-authored-by: aureliusbtc <82057759+aureliusbtc@users.noreply.github.com> --- packages/rest-api/CHANGELOG.md | 8 + packages/rest-api/package.json | 4 +- packages/sdk-router/CHANGELOG.md | 8 + packages/sdk-router/package.json | 2 +- packages/sdk-router/src/abi/SynapseCCTP.json | 767 ++++++++++++++++++ packages/sdk-router/src/operations/bridge.ts | 74 +- packages/sdk-router/src/router/router.ts | 19 + packages/sdk-router/src/router/routerSet.ts | 25 + .../src/router/synapseCCTPRouter.ts | 44 + .../src/router/synapseCCTPRouterSet.ts | 20 + .../sdk-router/src/router/synapseRouter.ts | 36 + .../sdk-router/src/router/synapseRouterSet.ts | 20 + packages/sdk-router/src/sdk.test.ts | 295 +++++++ packages/sdk-router/src/sdk.ts | 2 + packages/sdk-router/src/utils/logs.ts | 52 ++ packages/synapse-interface/CHANGELOG.md | 4 + packages/synapse-interface/package.json | 4 +- .../slices/transactions/updater.tsx | 14 +- 18 files changed, 1381 insertions(+), 17 deletions(-) create mode 100644 packages/sdk-router/src/abi/SynapseCCTP.json create mode 100644 packages/sdk-router/src/utils/logs.ts diff --git a/packages/rest-api/CHANGELOG.md b/packages/rest-api/CHANGELOG.md index beb2746464..3fbe07e202 100644 --- a/packages/rest-api/CHANGELOG.md +++ b/packages/rest-api/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.28](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.0.27...@synapsecns/rest-api@1.0.28) (2023-12-18) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + ## [1.0.27](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.0.26...@synapsecns/rest-api@1.0.27) (2023-12-12) **Note:** Version bump only for package @synapsecns/rest-api diff --git a/packages/rest-api/package.json b/packages/rest-api/package.json index b6ba5cb264..6eb4225106 100644 --- a/packages/rest-api/package.json +++ b/packages/rest-api/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/rest-api", - "version": "1.0.27", + "version": "1.0.28", "private": "true", "engines": { "node": ">=16.0.0" @@ -23,7 +23,7 @@ "@ethersproject/bignumber": "^5.7.0", "@ethersproject/providers": "^5.7.2", "@ethersproject/units": "5.7.0", - "@synapsecns/sdk-router": "^0.3.0", + "@synapsecns/sdk-router": "^0.3.1", "bignumber": "^1.1.0", "ethers": "5.7.2", "express": "^4.18.2", diff --git a/packages/sdk-router/CHANGELOG.md b/packages/sdk-router/CHANGELOG.md index e0c0de4307..a9de2dd6cc 100644 --- a/packages/sdk-router/CHANGELOG.md +++ b/packages/sdk-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.3.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/sdk-router@0.3.0...@synapsecns/sdk-router@0.3.1) (2023-12-18) + +**Note:** Version bump only for package @synapsecns/sdk-router + + + + + # [0.3.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/sdk-router@0.2.24...@synapsecns/sdk-router@0.3.0) (2023-12-12) diff --git a/packages/sdk-router/package.json b/packages/sdk-router/package.json index d9daf86230..594e10938f 100644 --- a/packages/sdk-router/package.json +++ b/packages/sdk-router/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/sdk-router", "description": "An SDK for interacting with the Synapse Protocol", - "version": "0.3.0", + "version": "0.3.1", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/sdk-router/src/abi/SynapseCCTP.json b/packages/sdk-router/src/abi/SynapseCCTP.json new file mode 100644 index 0000000000..b3034a7d88 --- /dev/null +++ b/packages/sdk-router/src/abi/SynapseCCTP.json @@ -0,0 +1,767 @@ +[ + { + "inputs": [ + { + "internalType": "contract ITokenMessenger", + "name": "tokenMessenger_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { "inputs": [], "name": "CCTPGasRescueFailed", "type": "error" }, + { "inputs": [], "name": "CCTPIncorrectChainId", "type": "error" }, + { "inputs": [], "name": "CCTPIncorrectConfig", "type": "error" }, + { "inputs": [], "name": "CCTPIncorrectDomain", "type": "error" }, + { "inputs": [], "name": "CCTPIncorrectGasAmount", "type": "error" }, + { "inputs": [], "name": "CCTPIncorrectProtocolFee", "type": "error" }, + { "inputs": [], "name": "CCTPIncorrectTokenAmount", "type": "error" }, + { "inputs": [], "name": "CCTPInsufficientAmount", "type": "error" }, + { "inputs": [], "name": "CCTPMessageNotReceived", "type": "error" }, + { "inputs": [], "name": "CCTPSymbolAlreadyAdded", "type": "error" }, + { "inputs": [], "name": "CCTPSymbolIncorrect", "type": "error" }, + { "inputs": [], "name": "CCTPTokenAlreadyAdded", "type": "error" }, + { "inputs": [], "name": "CCTPTokenNotFound", "type": "error" }, + { "inputs": [], "name": "CCTPZeroAddress", "type": "error" }, + { "inputs": [], "name": "CCTPZeroAmount", "type": "error" }, + { "inputs": [], "name": "CastOverflow", "type": "error" }, + { "inputs": [], "name": "ForwarderDeploymentFailed", "type": "error" }, + { "inputs": [], "name": "IncorrectRequestLength", "type": "error" }, + { "inputs": [], "name": "RemoteCCTPDeploymentNotSet", "type": "error" }, + { "inputs": [], "name": "UnknownRequestVersion", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ChainGasAirdropped", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "chainGasAmount", + "type": "uint256" + } + ], + "name": "ChainGasAmountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "originDomain", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "mintToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "requestID", + "type": "bytes32" + } + ], + "name": "CircleRequestFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "requestVersion", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "formattedRequest", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "requestID", + "type": "bytes32" + } + ], + "name": "CircleRequestSent", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "CircleTokenPoolSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "feeCollector", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "relayerFeeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "protocolFeeAmount", + "type": "uint256" + } + ], + "name": "FeeCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "relayer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "oldFeeCollector", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newFeeCollector", + "type": "address" + } + ], + "name": "FeeCollectorUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "FeesWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newProtocolFee", + "type": "uint256" + } + ], + "name": "ProtocolFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "remoteChainId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "remoteDomain", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "address", + "name": "remoteSynapseCCTP", + "type": "address" + } + ], + "name": "RemoteDomainConfigSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "TokenAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "relayerFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "minBaseFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "minSwapFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "maxFee", + "type": "uint256" + } + ], + "name": "TokenFeeSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "TokenRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "accumulatedFees", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "string", "name": "symbol", "type": "string" }, + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "relayerFee", "type": "uint256" }, + { "internalType": "uint256", "name": "minBaseFee", "type": "uint256" }, + { "internalType": "uint256", "name": "minSwapFee", "type": "uint256" }, + { "internalType": "uint256", "name": "maxFee", "type": "uint256" } + ], + "name": "addToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "bool", "name": "isSwap", "type": "bool" } + ], + "name": "calculateFeeAmount", + "outputs": [ + { "internalType": "uint256", "name": "fee", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "chainGasAmount", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "circleTokenPool", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "feeStructures", + "outputs": [ + { "internalType": "uint40", "name": "relayerFee", "type": "uint40" }, + { "internalType": "uint72", "name": "minBaseFee", "type": "uint72" }, + { "internalType": "uint72", "name": "minSwapFee", "type": "uint72" }, + { "internalType": "uint72", "name": "maxFee", "type": "uint72" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBridgeTokens", + "outputs": [ + { + "components": [ + { "internalType": "string", "name": "symbol", "type": "string" }, + { "internalType": "address", "name": "token", "type": "address" } + ], + "internalType": "struct BridgeToken[]", + "name": "bridgeTokens", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint32", "name": "remoteDomain", "type": "uint32" }, + { "internalType": "address", "name": "remoteToken", "type": "address" } + ], + "name": "getLocalToken", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner_", "type": "address" } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "requestID", "type": "bytes32" } + ], + "name": "isRequestFulfilled", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "localDomain", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "messageTransmitter", + "outputs": [ + { + "internalType": "contract IMessageTransmitter", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pauseSending", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolFee", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "message", "type": "bytes" }, + { "internalType": "bytes", "name": "signature", "type": "bytes" }, + { "internalType": "uint32", "name": "requestVersion", "type": "uint32" }, + { "internalType": "bytes", "name": "formattedRequest", "type": "bytes" } + ], + "name": "receiveCircleToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "relayerFeeCollectors", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "remoteDomainConfig", + "outputs": [ + { "internalType": "uint32", "name": "domain", "type": "uint32" }, + { "internalType": "address", "name": "synapseCCTP", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" } + ], + "name": "removeToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rescueGas", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "chainId", "type": "uint256" }, + { "internalType": "address", "name": "burnToken", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint32", "name": "requestVersion", "type": "uint32" }, + { "internalType": "bytes", "name": "swapParams", "type": "bytes" } + ], + "name": "sendCircleToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newChainGasAmount", + "type": "uint256" + } + ], + "name": "setChainGasAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "circleToken", "type": "address" }, + { "internalType": "address", "name": "pool", "type": "address" } + ], + "name": "setCircleTokenPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "feeCollector", "type": "address" } + ], + "name": "setFeeCollector", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "newProtocolFee", "type": "uint256" } + ], + "name": "setProtocolFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "remoteChainId", "type": "uint256" }, + { "internalType": "uint32", "name": "remoteDomain", "type": "uint32" }, + { + "internalType": "address", + "name": "remoteSynapseCCTP", + "type": "address" + } + ], + "name": "setRemoteDomainConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "relayerFee", "type": "uint256" }, + { "internalType": "uint256", "name": "minBaseFee", "type": "uint256" }, + { "internalType": "uint256", "name": "minSwapFee", "type": "uint256" }, + { "internalType": "uint256", "name": "maxFee", "type": "uint256" } + ], + "name": "setTokenFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "string", "name": "", "type": "string" }], + "name": "symbolToToken", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenMessenger", + "outputs": [ + { + "internalType": "contract ITokenMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "tokenToSymbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpauseSending", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" } + ], + "name": "withdrawProtocolFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" } + ], + "name": "withdrawRelayerFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/packages/sdk-router/src/operations/bridge.ts b/packages/sdk-router/src/operations/bridge.ts index 77d402220b..3aea7cd4a6 100644 --- a/packages/sdk-router/src/operations/bridge.ts +++ b/packages/sdk-router/src/operations/bridge.ts @@ -117,6 +117,46 @@ export async function bridgeQuote( return bestSet.finalizeBridgeRoute(bestRoute, deadline) } +/** + * Gets the unique Synapse txId for a bridge operation that happened within a given transaction. + * Synapse txId is known as "kappa" for SynapseBridge contract and "requestID" for SynapseCCTP contract. + * This function is meant to abstract away the differences between the two bridge modules. + * + * @param originChainId - The ID of the origin chain. + * @param bridgeModuleName - The name of the bridge module. + * @param txHash - The transaction hash of the bridge operation on the origin chain. + * @returns A promise that resolves to the unique Synapse txId of the bridge operation. + */ +export async function getSynapseTxId( + this: SynapseSDK, + originChainId: number, + bridgeModuleName: string, + txHash: string +): Promise { + return getRouterSet + .call(this, bridgeModuleName) + .getSynapseTxId(originChainId, txHash) +} + +/** + * Checks whether a bridge operation has been completed on the destination chain. + * + * @param destChainId - The ID of the destination chain. + * @param bridgeModuleName - The name of the bridge module. + * @param synapseTxId - The unique Synapse txId of the bridge operation. + * @returns A promise that resolves to a boolean indicating whether the bridge operation has been completed. + */ +export async function getBridgeTxStatus( + this: SynapseSDK, + destChainId: number, + bridgeModuleName: string, + synapseTxId: string +): Promise { + return getRouterSet + .call(this, bridgeModuleName) + .getBridgeTxStatus(destChainId, synapseTxId) +} + /** * Returns the name of the bridge module that emits the given event. * This will be either SynapseBridge or SynapseCCTP. @@ -143,22 +183,18 @@ export function getBridgeModuleName( * or the bridge token. * * @param originChainId - The ID of the origin chain. - * @param bridgeNoduleName - The name of the bridge module. + * @param bridgeModuleName - The name of the bridge module. * @returns - The estimated time for a bridge operation, in seconds. * @throws - Will throw an error if the bridge module is unknown for the given chain. */ export function getEstimatedTime( this: SynapseSDK, originChainId: number, - bridgeNoduleName: string + bridgeModuleName: string ): number { - if (this.synapseRouterSet.bridgeModuleName === bridgeNoduleName) { - return this.synapseRouterSet.getEstimatedTime(originChainId) - } - if (this.synapseCCTPRouterSet.bridgeModuleName === bridgeNoduleName) { - return this.synapseCCTPRouterSet.getEstimatedTime(originChainId) - } - throw new Error('Unknown bridge module') + return getRouterSet + .call(this, bridgeModuleName) + .getEstimatedTime(originChainId) } /** @@ -174,3 +210,23 @@ export async function getBridgeGas( ): Promise { return this.synapseRouterSet.getSynapseRouter(chainId).chainGasAmount() } + +/** + * Extracts the RouterSet from the SynapseSDK based on the given bridge module name. + * + * @param bridgeModuleName - The name of the bridge module, SynapseBridge or SynapseCCTP. + * @returns The corresponding RouterSet. + * @throws Will throw an error if the bridge module is unknown. + */ +export function getRouterSet( + this: SynapseSDK, + bridgeModuleName: string +): RouterSet { + if (this.synapseRouterSet.bridgeModuleName === bridgeModuleName) { + return this.synapseRouterSet + } + if (this.synapseCCTPRouterSet.bridgeModuleName === bridgeModuleName) { + return this.synapseCCTPRouterSet + } + throw new Error('Unknown bridge module') +} diff --git a/packages/sdk-router/src/router/router.ts b/packages/sdk-router/src/router/router.ts index f2bbc6ca19..29d42ff79d 100644 --- a/packages/sdk-router/src/router/router.ts +++ b/packages/sdk-router/src/router/router.ts @@ -61,6 +61,25 @@ export abstract class Router { destQuery: Query ): Promise + /** + * Returns the Synapse transaction ID for a given transaction hash on the current chain. + * This is used to track the status of a bridge transaction originating from the current chain. + * + * @param txHash - The transaction hash of the bridge transaction. + * @returns A promise that resolves to the Synapse transaction ID. + */ + abstract getSynapseTxId(txHash: string): Promise + + /** + * Checks whether a bridge transaction has been completed on the current chain. + * This is used to track the status of a bridge transaction originating from another chain, having + * current chain as the destination chain. + * + * @param synapseTxId - The unique Synapse txId of the bridge transaction. + * @returns A promise that resolves to a boolean indicating whether the bridge transaction has been completed. + */ + abstract getBridgeTxStatus(synapseTxId: string): Promise + /** * Fetches bridge tokens for a destination chain and output token. * diff --git a/packages/sdk-router/src/router/routerSet.ts b/packages/sdk-router/src/router/routerSet.ts index c5e6329ade..0ff81da1fb 100644 --- a/packages/sdk-router/src/router/routerSet.ts +++ b/packages/sdk-router/src/router/routerSet.ts @@ -71,6 +71,31 @@ export abstract class RouterSet { */ abstract getEstimatedTime(chainId: number): number + /** + * Returns the Synapse transaction ID for a given transaction hash on a given chain. + * This is used to track the status of a bridge transaction. + * + * @param originChainId - The ID of the origin chain. + * @param txHash - The transaction hash of the bridge transaction. + * @returns A promise that resolves to the Synapse transaction ID. + */ + abstract getSynapseTxId( + originChainId: number, + txHash: string + ): Promise + + /** + * Checks whether a bridge transaction has been completed on the destination chain. + * + * @param destChainId - The ID of the destination chain. + * @param synapseTxId - The unique Synapse txId of the bridge transaction. + * @returns A promise that resolves to a boolean indicating whether the bridge transaction has been completed. + */ + abstract getBridgeTxStatus( + destChainId: number, + synapseTxId: string + ): Promise + /** * Returns the existing Router instance for the given address on the given chain. * If the router address is not valid, it will return undefined. diff --git a/packages/sdk-router/src/router/synapseCCTPRouter.ts b/packages/sdk-router/src/router/synapseCCTPRouter.ts index 6e789dcdce..a25b3882ed 100644 --- a/packages/sdk-router/src/router/synapseCCTPRouter.ts +++ b/packages/sdk-router/src/router/synapseCCTPRouter.ts @@ -8,6 +8,8 @@ import cctpRouterAbi from '../abi/SynapseCCTPRouter.json' import { SynapseCCTPRouter as SynapseCCTPRouterContract } from '../typechain/SynapseCCTPRouter' import { Router } from './router' import { Query, narrowToCCTPRouterQuery, reduceToQuery } from './query' +import cctpAbi from '../abi/SynapseCCTP.json' +import { getMatchingTxLog } from '../utils/logs' import { BigintIsh } from '../constants' import { BridgeToken, @@ -27,6 +29,10 @@ export class SynapseCCTPRouter extends Router { public readonly address: string private readonly routerContract: SynapseCCTPRouterContract + private cctpContractCache: Contract | undefined + + // All possible events emitted by the SynapseCCTP contract in the origin transaction + private readonly originEvents = ['CircleRequestSent'] constructor(chainId: number, provider: Provider, address: string) { // Parent constructor throws if chainId or provider are undefined @@ -114,4 +120,42 @@ export class SynapseCCTPRouter extends Router { narrowToCCTPRouterQuery(destQuery) ) } + + /** + * @inheritdoc Router.getSynapseTxId + */ + public async getSynapseTxId(txHash: string): Promise { + const cctpContract = await this.getCctpContract() + const cctpLog = await getMatchingTxLog( + this.provider, + txHash, + cctpContract, + this.originEvents + ) + // RequestID always exists in the log as we are using the correct ABI + const parsedLog = cctpContract.interface.parseLog(cctpLog) + return parsedLog.args.requestID + } + + /** + * @inheritdoc Router.getBridgeTxStatus + */ + public async getBridgeTxStatus(synapseTxId: string): Promise { + const cctpContract = await this.getCctpContract() + return cctpContract.isRequestFulfilled(synapseTxId) + } + + private async getCctpContract(): Promise { + // Populate the cache if necessary + if (!this.cctpContractCache) { + const cctpAddress = await this.routerContract.synapseCCTP() + this.cctpContractCache = new Contract( + cctpAddress, + new Interface(cctpAbi), + this.provider + ) + } + // Return the cached contract + return this.cctpContractCache + } } diff --git a/packages/sdk-router/src/router/synapseCCTPRouterSet.ts b/packages/sdk-router/src/router/synapseCCTPRouterSet.ts index b6252c7d1b..5f2498d9c6 100644 --- a/packages/sdk-router/src/router/synapseCCTPRouterSet.ts +++ b/packages/sdk-router/src/router/synapseCCTPRouterSet.ts @@ -28,6 +28,26 @@ export class SynapseCCTPRouterSet extends RouterSet { return medianTime } + /** + * @inheritdoc RouterSet.getSynapseTxId + */ + public async getSynapseTxId( + originChainId: number, + txHash: string + ): Promise { + return this.getSynapseCCTPRouter(originChainId).getSynapseTxId(txHash) + } + + /** + * @inheritdoc RouterSet.getBridgeTxStatus + */ + public async getBridgeTxStatus( + destChainId: number, + synapseTxId: string + ): Promise { + return this.getSynapseCCTPRouter(destChainId).getBridgeTxStatus(synapseTxId) + } + /** * Returns the existing SynapseCCTPRouter instance for the given chain. * diff --git a/packages/sdk-router/src/router/synapseRouter.ts b/packages/sdk-router/src/router/synapseRouter.ts index d32eb83999..bde39244df 100644 --- a/packages/sdk-router/src/router/synapseRouter.ts +++ b/packages/sdk-router/src/router/synapseRouter.ts @@ -3,6 +3,7 @@ import invariant from 'tiny-invariant' import { Contract, PopulatedTransaction } from '@ethersproject/contracts' import { Interface } from '@ethersproject/abi' import { BigNumber } from '@ethersproject/bignumber' +import { solidityKeccak256 } from 'ethers/lib/utils' import routerAbi from '../abi/SynapseRouter.json' import { @@ -24,6 +25,7 @@ import { reduceToFeeConfig, reduceToPoolToken, } from './types' +import { getMatchingTxLog } from '../utils/logs' /** * Wraps [tokens, lpToken] returned by the SynapseRouter contract into a PoolInfo object. @@ -59,6 +61,16 @@ export class SynapseRouter extends Router { private readonly routerContract: SynapseRouterContract private bridgeContractCache: Contract | undefined + // All possible events emitted by the SynapseBridge contract in the origin transaction (in alphabetical order) + private readonly originEvents = [ + 'TokenDeposit', + 'TokenDepositAndSwap', + 'TokenRedeem', + 'TokenRedeemAndRemove', + 'TokenRedeemAndSwap', + 'TokenRedeemV2', + ] + constructor(chainId: number, provider: Provider, address: string) { // Parent constructor throws if chainId or provider are undefined super(chainId, provider) @@ -137,6 +149,30 @@ export class SynapseRouter extends Router { ) } + /** + * @inheritdoc Router.getSynapseTxId + */ + public async getSynapseTxId(txHash: string): Promise { + // Check that the transaction hash refers to an origin transaction + const bridgeContract = await this.getBridgeContract() + await getMatchingTxLog( + this.provider, + txHash, + bridgeContract, + this.originEvents + ) + // Once we know the transaction is an origin transaction, we can calculate the Synapse txId + return solidityKeccak256(['string'], [txHash]) + } + + /** + * @inheritdoc Router.getBridgeTxStatus + */ + public async getBridgeTxStatus(synapseTxId: string): Promise { + const bridgeContract = await this.getBridgeContract() + return bridgeContract.kappaExists(synapseTxId) + } + // ═════════════════════════════════════════ SYNAPSE ROUTER (V1) ONLY ══════════════════════════════════════════════ private async getBridgeContract(): Promise { diff --git a/packages/sdk-router/src/router/synapseRouterSet.ts b/packages/sdk-router/src/router/synapseRouterSet.ts index 8d0209bcfd..b84f21b7e2 100644 --- a/packages/sdk-router/src/router/synapseRouterSet.ts +++ b/packages/sdk-router/src/router/synapseRouterSet.ts @@ -36,6 +36,26 @@ export class SynapseRouterSet extends RouterSet { return medianTime } + /** + * @inheritdoc RouterSet.getSynapseTxId + */ + public async getSynapseTxId( + originChainId: number, + txHash: string + ): Promise { + return this.getSynapseRouter(originChainId).getSynapseTxId(txHash) + } + + /** + * @inheritdoc RouterSet.getBridgeTxStatus + */ + public async getBridgeTxStatus( + destChainId: number, + synapseTxId: string + ): Promise { + return this.getSynapseRouter(destChainId).getBridgeTxStatus(synapseTxId) + } + /** * Returns the existing SynapseRouter instance for the given chain. * diff --git a/packages/sdk-router/src/sdk.test.ts b/packages/sdk-router/src/sdk.test.ts index 1df69d660b..e8fce8bb2e 100644 --- a/packages/sdk-router/src/sdk.test.ts +++ b/packages/sdk-router/src/sdk.test.ts @@ -29,6 +29,7 @@ import { SupportedChainId, } from './constants' import { BridgeQuote, FeeConfig, RouterQuery, SwapQuote } from './router' +import * as operations from './operations' const expectCorrectFeeConfig = (feeConfig: FeeConfig) => { expect(feeConfig).toBeDefined() @@ -693,6 +694,276 @@ describe('SynapseSDK', () => { ) }) + describe('Bridge Tx Status', () => { + const synapse = new SynapseSDK( + [SupportedChainId.ARBITRUM, SupportedChainId.ETH], + [arbProvider, ethProvider] + ) + + // https://etherscan.io/tx/0xe3f0f0c1d139c48730492c900f9978449d70c0939c654d5abbfd6b191f9c7b3d + // https://arbiscan.io/tx/0xb13d5c9156e2d88662fa2f252bd2e1d77d768f0de9d27ca60a79e40b493f6ef2 + const bridgeEthToArbTx = { + txHash: + '0xe3f0f0c1d139c48730492c900f9978449d70c0939c654d5abbfd6b191f9c7b3d', + synapseTxId: + '0x2f223fb1509f04f777b5c9dd2287931b6e63d994a6a697db7a08cfbe784b5e90', + } + + // https://arbiscan.io/tx/0xe226c7e38e4b83072aa9d947e533be32c8bb38120bbdd8f490c5c6a5894e62c9 + // https://etherscan.io/tx/0xb88feb2a92690448b840851dff41dbc7cdc975c1fb740f0523b5c2e407ac9f38 + const bridgeArbToEthTx = { + txHash: + '0xe226c7e38e4b83072aa9d947e533be32c8bb38120bbdd8f490c5c6a5894e62c9', + synapseTxId: + '0xf7b8085d96b1ea3f6bf7a07ad93d1861b8fcd551ef56665d6a22c9fb7633a097', + } + + // https://etherscan.io/tx/0x1a25b0dfde1e2cc43f1dc659ba60f2b8e7ff8177555773fea0c4fba2d6e9c393 + // https://arbiscan.io/tx/0x0166c1e99b0ec8942ed10527cd7ac9003111ee697e0c0519312228e669a61378 + const cctpEthToArbTx = { + txHash: + '0x1a25b0dfde1e2cc43f1dc659ba60f2b8e7ff8177555773fea0c4fba2d6e9c393', + synapseTxId: + '0x492b923b5a0ace2715a8d0a80fb93c094bf6d35b142a010bdc3761b8613439fc', + } + + // https://arbiscan.io/tx/0x2a6d04ba5a48331454f00d136b3666869d03f004395fea25d97d42715c119096 + // https://etherscan.io/tx/0xefb946d2acf8343ac5526de66de498e0d5f70ae73c81b833181616ee058a22d7 + const cctpArbToEthTx = { + txHash: + '0x2a6d04ba5a48331454f00d136b3666869d03f004395fea25d97d42715c119096', + synapseTxId: + '0xed98b02f712c940d3b37a1aa9005a5986ecefa5cdbb4505118a22ae65d4903af', + } + + describe('getSynapseTxId', () => { + describe('SynapseBridge', () => { + const ethSynBridge = '0x2796317b0fF8538F253012862c06787Adfb8cEb6' + const events = + 'TokenDeposit, TokenDepositAndSwap, TokenRedeem, TokenRedeemAndRemove, TokenRedeemAndSwap, TokenRedeemV2' + + it('ETH -> ARB', async () => { + const synapseTxId = await synapse.getSynapseTxId( + SupportedChainId.ETH, + 'SynapseBridge', + bridgeEthToArbTx.txHash + ) + expect(synapseTxId).toEqual(bridgeEthToArbTx.synapseTxId) + }) + + it('ARB -> ETH', async () => { + const synapseTxId = await synapse.getSynapseTxId( + SupportedChainId.ARBITRUM, + 'SynapseBridge', + bridgeArbToEthTx.txHash + ) + expect(synapseTxId).toEqual(bridgeArbToEthTx.synapseTxId) + }) + + it('Throws when given a txHash that does not exist', async () => { + // Use txHash for another chain + await expect( + synapse.getSynapseTxId( + SupportedChainId.ETH, + 'SynapseBridge', + bridgeArbToEthTx.txHash + ) + ).rejects.toThrow('Failed to get transaction receipt') + }) + + it('Throws when origin tx does not refer to SynapseBridge', async () => { + const errorMsg = + `Contract ${ethSynBridge} in transaction ${cctpEthToArbTx.txHash}` + + ` did not emit any of the expected events: ${events}` + await expect( + synapse.getSynapseTxId( + SupportedChainId.ETH, + 'SynapseBridge', + cctpEthToArbTx.txHash + ) + ).rejects.toThrow(errorMsg) + }) + + it('Throws when given a destination tx', async () => { + // Destination tx hash for ARB -> ETH + const txHash = + '0xefb946d2acf8343ac5526de66de498e0d5f70ae73c81b833181616ee058a22d7' + const errorMsg = + `Contract ${ethSynBridge} in transaction ${txHash}` + + ` did not emit any of the expected events: ${events}` + await expect( + synapse.getSynapseTxId( + SupportedChainId.ETH, + 'SynapseBridge', + txHash + ) + ).rejects.toThrow(errorMsg) + }) + }) + + describe('SynapseCCTP', () => { + const ethSynCCTP = '0x12715a66773BD9C54534a01aBF01d05F6B4Bd35E' + const events = 'CircleRequestSent' + + it('ETH -> ARB', async () => { + const synapseTxId = await synapse.getSynapseTxId( + SupportedChainId.ETH, + 'SynapseCCTP', + cctpEthToArbTx.txHash + ) + expect(synapseTxId).toEqual(cctpEthToArbTx.synapseTxId) + }) + + it('ARB -> ETH', async () => { + const synapseTxId = await synapse.getSynapseTxId( + SupportedChainId.ARBITRUM, + 'SynapseCCTP', + cctpArbToEthTx.txHash + ) + expect(synapseTxId).toEqual(cctpArbToEthTx.synapseTxId) + }) + + it('Throws when given a txHash that does not exist', async () => { + // Use txHash for another chain + await expect( + synapse.getSynapseTxId( + SupportedChainId.ETH, + 'SynapseCCTP', + cctpArbToEthTx.txHash + ) + ).rejects.toThrow('Failed to get transaction receipt') + }) + + it('Throws when origin tx does not refer to SynapseCCTP', async () => { + const errorMsg = + `Contract ${ethSynCCTP} in transaction ${bridgeEthToArbTx.txHash}` + + ` did not emit any of the expected events: ${events}` + await expect( + synapse.getSynapseTxId( + SupportedChainId.ETH, + 'SynapseCCTP', + bridgeEthToArbTx.txHash + ) + ).rejects.toThrow(errorMsg) + }) + + it('Throws when given a destination tx', async () => { + // Destination tx hash for ARB -> ETH + const txHash = + '0xefb946d2acf8343ac5526de66de498e0d5f70ae73c81b833181616ee058a22d7' + const errorMsg = + `Contract ${ethSynCCTP} in transaction ${txHash}` + + ` did not emit any of the expected events: ${events}` + await expect( + synapse.getSynapseTxId(SupportedChainId.ETH, 'SynapseCCTP', txHash) + ).rejects.toThrow(errorMsg) + }) + }) + + it('Throws when bridge module name is invalid', async () => { + await expect( + synapse.getSynapseTxId( + SupportedChainId.ETH, + 'SynapseSynapse', + bridgeEthToArbTx.txHash + ) + ).rejects.toThrow('Unknown bridge module') + }) + }) + + describe('getBridgeTxStatus', () => { + describe('SynapseBridge', () => { + it('ETH -> ARB', async () => { + const txStatus = await synapse.getBridgeTxStatus( + SupportedChainId.ARBITRUM, + 'SynapseBridge', + bridgeEthToArbTx.synapseTxId + ) + expect(txStatus).toBe(true) + }) + + it('ARB -> ETH', async () => { + const txStatus = await synapse.getBridgeTxStatus( + SupportedChainId.ETH, + 'SynapseBridge', + bridgeArbToEthTx.synapseTxId + ) + expect(txStatus).toBe(true) + }) + + it('Returns false when unknown synapseTxId', async () => { + // Using txHash instead of synapseTxId + const txStatus = await synapse.getBridgeTxStatus( + SupportedChainId.ETH, + 'SynapseBridge', + bridgeArbToEthTx.txHash + ) + expect(txStatus).toBe(false) + }) + + it('Returns false when origin chain is used instead of destination', async () => { + // First argument should be destination chainId + const txStatus = await synapse.getBridgeTxStatus( + SupportedChainId.ETH, + 'SynapseBridge', + bridgeEthToArbTx.synapseTxId + ) + expect(txStatus).toBe(false) + }) + }) + + describe('SynapseCCTP', () => { + it('ETH -> ARB', async () => { + const txStatus = await synapse.getBridgeTxStatus( + SupportedChainId.ARBITRUM, + 'SynapseCCTP', + cctpEthToArbTx.synapseTxId + ) + expect(txStatus).toBe(true) + }) + + it('ARB -> ETH', async () => { + const txStatus = await synapse.getBridgeTxStatus( + SupportedChainId.ETH, + 'SynapseCCTP', + cctpArbToEthTx.synapseTxId + ) + expect(txStatus).toBe(true) + }) + + it('Returns false when unknown synapseTxId', async () => { + // Using txHash instead of synapseTxId + const txStatus = await synapse.getBridgeTxStatus( + SupportedChainId.ETH, + 'SynapseCCTP', + cctpArbToEthTx.txHash + ) + expect(txStatus).toBe(false) + }) + + it('Returns false when origin chain is used instead of destination', async () => { + // First argument should be destination chainId + const txStatus = await synapse.getBridgeTxStatus( + SupportedChainId.ETH, + 'SynapseCCTP', + cctpEthToArbTx.synapseTxId + ) + expect(txStatus).toBe(false) + }) + }) + + it('Throws when bridge module name is invalid', async () => { + await expect( + synapse.getBridgeTxStatus( + SupportedChainId.ETH, + 'SynapseSynapse', + bridgeEthToArbTx.txHash + ) + ).rejects.toThrow('Unknown bridge module') + }) + }) + }) + describe('getBridgeModuleName', () => { const synapse = new SynapseSDK([], []) @@ -1022,4 +1293,28 @@ describe('SynapseSDK', () => { ) }) }) + + describe('Internal functions', () => { + const synapse = new SynapseSDK( + [SupportedChainId.ARBITRUM, SupportedChainId.ETH], + [arbProvider, ethProvider] + ) + describe('getRouterSet', () => { + it('Returns correct set for SynapseBridge', () => { + const routerSet = operations.getRouterSet.call(synapse, 'SynapseBridge') + expect(routerSet).toEqual(synapse.synapseRouterSet) + }) + + it('Returns correct set for SynapseCCTP', () => { + const routerSet = operations.getRouterSet.call(synapse, 'SynapseCCTP') + expect(routerSet).toEqual(synapse.synapseCCTPRouterSet) + }) + + it('Throws when bridge module name is invalid', () => { + expect(() => + operations.getRouterSet.call(synapse, 'SynapseSynapse') + ).toThrow('Unknown bridge module') + }) + }) + }) }) diff --git a/packages/sdk-router/src/sdk.ts b/packages/sdk-router/src/sdk.ts index 7cd4a0153e..dfde1e2a55 100644 --- a/packages/sdk-router/src/sdk.ts +++ b/packages/sdk-router/src/sdk.ts @@ -48,6 +48,8 @@ class SynapseSDK { public bridgeQuote = operations.bridgeQuote public getBridgeModuleName = operations.getBridgeModuleName public getEstimatedTime = operations.getEstimatedTime + public getSynapseTxId = operations.getSynapseTxId + public getBridgeTxStatus = operations.getBridgeTxStatus public getBridgeGas = operations.getBridgeGas diff --git a/packages/sdk-router/src/utils/logs.ts b/packages/sdk-router/src/utils/logs.ts new file mode 100644 index 0000000000..2b8c2f1e4e --- /dev/null +++ b/packages/sdk-router/src/utils/logs.ts @@ -0,0 +1,52 @@ +import { Log, Provider } from '@ethersproject/abstract-provider' +import { Contract } from '@ethersproject/contracts' +import { Interface } from '@ethersproject/abi' + +/** + * Extracts the first log from a transaction receipt that matches + * the provided contract and any of the provided event names. + * + * @param provider - Ethers provider for the network + * @param txHash - Transaction hash + * @param contract - Contract that should have emitted the event + * @param eventNames - Names of the events that could have been emitted + * @returns The first log that matches the contract and any of the event names + * @throws If the transaction receipt cannot be retrieved, or if no matching log is found + */ +export const getMatchingTxLog = async ( + provider: Provider, + txHash: string, + contract: Contract, + eventNames: string[] +): Promise => { + const txReceipt = await provider.getTransactionReceipt(txHash) + if (!txReceipt) { + throw new Error('Failed to get transaction receipt') + } + const topics = getEventTopics(contract.interface, eventNames) + // Find the log with the correct contract address and topic matching any of the provided topics + const matchingLog = txReceipt.logs.find((log) => { + return log.address === contract.address && topics.includes(log.topics[0]) + }) + if (!matchingLog) { + // Throw an error and include the event names in the message + throw new Error( + `Contract ${ + contract.address + } in transaction ${txHash} did not emit any of the expected events: ${eventNames.join( + ', ' + )}` + ) + } + return matchingLog +} + +const getEventTopics = ( + contractInterface: Interface, + eventNames: string[] +): string[] => { + // Filter events that match the provided event names and map them to their topics + return Object.values(contractInterface.events) + .filter((event) => eventNames.includes(event.name)) + .map((event) => contractInterface.getEventTopic(event)) +} diff --git a/packages/synapse-interface/CHANGELOG.md b/packages/synapse-interface/CHANGELOG.md index 38b4f82a17..c5fb02aff0 100644 --- a/packages/synapse-interface/CHANGELOG.md +++ b/packages/synapse-interface/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.209](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.1.208...@synapsecns/synapse-interface@0.1.209) (2023-12-18) + +**Note:** Version bump only for package @synapsecns/synapse-interface + ## [0.1.208](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.1.207...@synapsecns/synapse-interface@0.1.208) (2023-12-14) **Note:** Version bump only for package @synapsecns/synapse-interface diff --git a/packages/synapse-interface/package.json b/packages/synapse-interface/package.json index b506fab28b..5c79242cf1 100644 --- a/packages/synapse-interface/package.json +++ b/packages/synapse-interface/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/synapse-interface", - "version": "0.1.208", + "version": "0.1.209", "private": true, "engines": { "node": ">=16.0.0" @@ -42,7 +42,7 @@ "@reduxjs/toolkit": "^1.9.5", "@rtk-query/graphql-request-base-query": "^2.2.0", "@segment/analytics-next": "^1.53.0", - "@synapsecns/sdk-router": "^0.3.0", + "@synapsecns/sdk-router": "^0.3.1", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.2", diff --git a/packages/synapse-interface/slices/transactions/updater.tsx b/packages/synapse-interface/slices/transactions/updater.tsx index 8d6c4129e4..180062b24b 100644 --- a/packages/synapse-interface/slices/transactions/updater.tsx +++ b/packages/synapse-interface/slices/transactions/updater.tsx @@ -39,6 +39,8 @@ import { checkTransactionsExist } from '@/utils/checkTransactionsExist' const queryHistoricalTime: number = getTimeMinutesBeforeNow(oneMonthInMinutes) const queryPendingTime: number = getTimeMinutesBeforeNow(oneDayInMinutes) +const POLLING_INTERVAL: number = 30000 // in ms + export default function Updater(): null { const dispatch = useAppDispatch() const { @@ -59,10 +61,12 @@ export default function Updater(): null { }: PortfolioState = usePortfolioState() const [fetchUserHistoricalActivity, fetchedHistoricalActivity] = - useLazyGetUserHistoricalActivityQuery({ pollingInterval: 3000000 }) + useLazyGetUserHistoricalActivityQuery({ pollingInterval: POLLING_INTERVAL }) const [fetchUserPendingActivity, fetchedPendingActivity] = - useLazyGetUserPendingTransactionsQuery({ pollingInterval: 3000000 }) + useLazyGetUserPendingTransactionsQuery({ + pollingInterval: POLLING_INTERVAL, + }) const { address } = useAccount({ onDisconnect() { @@ -445,7 +449,11 @@ export default function Updater(): null { } ) } - }, [fallbackQueryPendingTransactions, fallbackQueryHistoricalTransactions]) + }, [ + fallbackQueryPendingTransactions, + fallbackQueryHistoricalTransactions, + userHistoricalTransactions, + ]) return null }