diff --git a/package.json b/package.json index 154443a6..fb0e1b98 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "@railgun-community/poi-safe-broadcaster-example", - "version": "7.2.0", + "version": "8.0.0", "description": "", "main": "dist/main.js", - "packageManager": "yarn@1.22.19", + "packageManager": "yarn@1.22.22", "engines": { "node": ">=16.15.0" }, @@ -31,8 +31,8 @@ "dependencies": { "@0x/contract-addresses": "^8.0.0", "@noble/ed25519": "^1.7.1", - "@railgun-community/shared-models": "7.5.0", - "@railgun-community/wallet": "10.3.3", + "@railgun-community/shared-models": "^8.0.0", + "@railgun-community/wallet": "^10.8.1", "@walletconnect/jsonrpc-types": "^1.0.2", "@walletconnect/jsonrpc-utils": "^1.0.4", "axios": "1.7.2", @@ -70,4 +70,4 @@ "tsc-alias": "^1.8.2", "typescript": "^4.9.4" } -} +} \ No newline at end of file diff --git a/src/server/abi/abi.ts b/src/server/abi/abi.ts index 00a91061..e5e84f52 100644 --- a/src/server/abi/abi.ts +++ b/src/server/abi/abi.ts @@ -1,4 +1,5 @@ import ABI_ERC20 from './json/erc20.json'; import ABI_PERMIT2 from './json/permit2.json'; +import ABI_RELAY_ADAPT from './json/relay.json'; -export { ABI_ERC20, ABI_PERMIT2 }; +export { ABI_ERC20, ABI_PERMIT2, ABI_RELAY_ADAPT }; diff --git a/src/server/abi/json/relay.json b/src/server/abi/json/relay.json new file mode 100644 index 00000000..ba2836d1 --- /dev/null +++ b/src/server/abi/json/relay.json @@ -0,0 +1,748 @@ +[ + { + "inputs": [ + { + "internalType": "contract RailgunSmartWallet", + "name": "_railgun", + "type": "address" + }, + { + "internalType": "contract IWBase", + "name": "_wBase", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "callIndex", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "revertReason", + "type": "bytes" + } + ], + "name": "CallFailed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "callIndex", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "revertReason", + "type": "bytes" + } + ], + "name": "CallError", + "type": "event" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "a", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256[2]", + "name": "x", + "type": "uint256[2]" + }, + { + "internalType": "uint256[2]", + "name": "y", + "type": "uint256[2]" + } + ], + "internalType": "struct G2Point", + "name": "b", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "c", + "type": "tuple" + } + ], + "internalType": "struct SnarkProof", + "name": "proof", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "nullifiers", + "type": "bytes32[]" + }, + { + "internalType": "bytes32[]", + "name": "commitments", + "type": "bytes32[]" + }, + { + "components": [ + { + "internalType": "uint16", + "name": "treeNumber", + "type": "uint16" + }, + { + "internalType": "uint72", + "name": "minGasPrice", + "type": "uint72" + }, + { + "internalType": "enum UnshieldType", + "name": "unshield", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "chainID", + "type": "uint64" + }, + { + "internalType": "address", + "name": "adaptContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "adaptParams", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bytes32[4]", + "name": "ciphertext", + "type": "bytes32[4]" + }, + { + "internalType": "bytes32", + "name": "blindedSenderViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "blindedReceiverViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "annotationData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "memo", + "type": "bytes" + } + ], + "internalType": "struct CommitmentCiphertext[]", + "name": "commitmentCiphertext", + "type": "tuple[]" + } + ], + "internalType": "struct BoundParams", + "name": "boundParams", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "npk", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenSubID", + "type": "uint256" + } + ], + "internalType": "struct TokenData", + "name": "token", + "type": "tuple" + }, + { + "internalType": "uint120", + "name": "value", + "type": "uint120" + } + ], + "internalType": "struct CommitmentPreimage", + "name": "unshieldPreimage", + "type": "tuple" + } + ], + "internalType": "struct Transaction[]", + "name": "_transactions", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "bytes31", + "name": "random", + "type": "bytes31" + }, + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "minGasLimit", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "internalType": "struct RelayAdapt.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "internalType": "struct RelayAdapt.ActionData", + "name": "_actionData", + "type": "tuple" + } + ], + "name": "getAdaptParams", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "internalType": "struct RelayAdapt.Call[]", + "name": "_calls", + "type": "tuple[]" + } + ], + "name": "multicall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "railgun", + "outputs": [ + { + "internalType": "contract RailgunSmartWallet", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "a", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256[2]", + "name": "x", + "type": "uint256[2]" + }, + { + "internalType": "uint256[2]", + "name": "y", + "type": "uint256[2]" + } + ], + "internalType": "struct G2Point", + "name": "b", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "c", + "type": "tuple" + } + ], + "internalType": "struct SnarkProof", + "name": "proof", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "nullifiers", + "type": "bytes32[]" + }, + { + "internalType": "bytes32[]", + "name": "commitments", + "type": "bytes32[]" + }, + { + "components": [ + { + "internalType": "uint16", + "name": "treeNumber", + "type": "uint16" + }, + { + "internalType": "uint72", + "name": "minGasPrice", + "type": "uint72" + }, + { + "internalType": "enum UnshieldType", + "name": "unshield", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "chainID", + "type": "uint64" + }, + { + "internalType": "address", + "name": "adaptContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "adaptParams", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bytes32[4]", + "name": "ciphertext", + "type": "bytes32[4]" + }, + { + "internalType": "bytes32", + "name": "blindedSenderViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "blindedReceiverViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "annotationData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "memo", + "type": "bytes" + } + ], + "internalType": "struct CommitmentCiphertext[]", + "name": "commitmentCiphertext", + "type": "tuple[]" + } + ], + "internalType": "struct BoundParams", + "name": "boundParams", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "npk", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenSubID", + "type": "uint256" + } + ], + "internalType": "struct TokenData", + "name": "token", + "type": "tuple" + }, + { + "internalType": "uint120", + "name": "value", + "type": "uint120" + } + ], + "internalType": "struct CommitmentPreimage", + "name": "unshieldPreimage", + "type": "tuple" + } + ], + "internalType": "struct Transaction[]", + "name": "_transactions", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "bytes31", + "name": "random", + "type": "bytes31" + }, + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "minGasLimit", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "internalType": "struct RelayAdapt.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "internalType": "struct RelayAdapt.ActionData", + "name": "_actionData", + "type": "tuple" + } + ], + "name": "relay", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "npk", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenSubID", + "type": "uint256" + } + ], + "internalType": "struct TokenData", + "name": "token", + "type": "tuple" + }, + { + "internalType": "uint120", + "name": "value", + "type": "uint120" + } + ], + "internalType": "struct CommitmentPreimage", + "name": "preimage", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32[3]", + "name": "encryptedBundle", + "type": "bytes32[3]" + }, + { + "internalType": "bytes32", + "name": "shieldKey", + "type": "bytes32" + } + ], + "internalType": "struct ShieldCiphertext", + "name": "ciphertext", + "type": "tuple" + } + ], + "internalType": "struct ShieldRequest[]", + "name": "_shieldRequests", + "type": "tuple[]" + } + ], + "name": "shield", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenSubID", + "type": "uint256" + } + ], + "internalType": "struct TokenData", + "name": "token", + "type": "tuple" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "internalType": "struct RelayAdapt.TokenTransfer[]", + "name": "_transfers", + "type": "tuple[]" + } + ], + "name": "transfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "unwrapBase", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wBase", + "outputs": [ + { + "internalType": "contract IWBase", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "wrapBase", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/server/api/0x/0x-fetch.ts b/src/server/api/0x/0x-fetch.ts index 27b11228..ef130384 100644 --- a/src/server/api/0x/0x-fetch.ts +++ b/src/server/api/0x/0x-fetch.ts @@ -6,8 +6,8 @@ import { NetworkChainID } from '../../config/config-chains'; import configDefaults from '../../config/config-defaults'; export enum ZeroXApiEndpoint { - PriceLookup = 'swap/v1/price', - GetSwapQuote = 'swap/v1/quote', + PriceLookup = 'swap/permit2/price', + GetSwapQuote = 'swap/allowance-holder/quote', } export const getStablecoinReferenceSymbol = ( @@ -44,19 +44,14 @@ const zeroXApiUrl = (chain: BroadcasterChain): string => { case ChainType.EVM: { switch (chain.id) { case NetworkChainID.Ethereum: - return 'https://api.0x.org/'; - case NetworkChainID.EthereumGoerli: - return 'https://goerli.api.0x.org/'; case NetworkChainID.BNBChain: - return 'https://bsc.api.0x.org/'; case NetworkChainID.PolygonPOS: - return 'https://polygon.api.0x.org/'; case NetworkChainID.Arbitrum: - return 'https://arbitrum.api.0x.org/'; case NetworkChainID.PolygonMumbai: - return 'https://mumbai.api.0x.org/'; - case NetworkChainID.ArbitrumGoerli: case NetworkChainID.EthereumSepolia: + return 'https://api.0x.org/'; + case NetworkChainID.EthereumGoerli: + case NetworkChainID.ArbitrumGoerli: case NetworkChainID.PolygonAmoy: case NetworkChainID.Hardhat: throw new Error(`No 0x API URL for chain ${chain.type}:${chain.id}`); @@ -101,7 +96,6 @@ export const getZeroXData = async ( if (!apiKey) { throw new Error('Requires 0x API Key for prices/quotes.'); } - try { const rsp = await axios.get(url, { method: 'GET', @@ -109,6 +103,7 @@ export const getZeroXData = async ( Accept: 'application/json', 'Content-Type': 'application/json', '0x-api-key': apiKey, + '0x-version': 'v2' }, }); return rsp.data; diff --git a/src/server/api/0x/0x-price.ts b/src/server/api/0x/0x-price.ts index 79b19d09..3070983f 100644 --- a/src/server/api/0x/0x-price.ts +++ b/src/server/api/0x/0x-price.ts @@ -1,7 +1,7 @@ import { isDefined } from '@railgun-community/shared-models'; import { BroadcasterChain } from '../../../models/chain-models'; import { delay, promiseTimeout } from '../../../util/promise-utils'; -import { tokenForAddress } from '../../tokens/network-tokens'; +import { tokenForAddress, tokenForSymbol } from '../../tokens/network-tokens'; import { TokenPrice, TokenPriceUpdater } from '../../tokens/token-price-cache'; import { ZeroXApiEndpoint, @@ -20,10 +20,13 @@ let ZERO_X_PRICE_LOOKUP_DELAY = 1500; const refreshLocks: NumMapType> = {}; export type ZeroXPriceData = { + minBuyAmount: string; + sellAmount: string; price: string; }; export type ZeroXPriceParams = { + chainId: string; sellToken: string; buyToken: string; sellAmount: string; @@ -45,7 +48,6 @@ const zeroXPriceLookupByAddress = async ( ): Promise> => { try { const { decimals, symbol } = tokenForAddress(chain, tokenAddress); - // TODO: This depends on DAI being stable at $1. // As we've seen, this isn't the safest methodology. // However, if the price of DAI drops, the broadcasted fees can only increase, @@ -55,19 +57,23 @@ const zeroXPriceLookupByAddress = async ( if (symbol === stablecoinSymbol) { return { price: 1 }; } + const stablecoinToken = tokenForSymbol(chain, stablecoinSymbol); + const sellAmount = (10n ** decimals).toString(10); const params: ZeroXPriceParams = { + chainId: chain.id.toString(), sellToken: tokenAddress, - buyToken: stablecoinSymbol, + buyToken: stablecoinToken.address, sellAmount, // 1 token }; - const { price } = await getZeroXData( + const zeroXPriceData= await getZeroXData( ZeroXApiEndpoint.PriceLookup, chain, params, ); + const formattedPrice = formatPriceResponse(zeroXPriceData); return { - price: parseFloat(price), + price: parseFloat(formattedPrice), }; } catch (err) { return undefined; @@ -118,3 +124,9 @@ export const zeroXUpdatePricesByAddresses = async ( refreshLocks[chain.type][chain.id] = false; }; + + +export const formatPriceResponse = (zeroXPriceData: ZeroXPriceData)=>{ + const formattedPrice = BigInt(zeroXPriceData.minBuyAmount) / BigInt(zeroXPriceData.sellAmount) + return formattedPrice.toString(10); +} \ No newline at end of file diff --git a/src/server/api/0x/__tests__/0x-price.test.ts b/src/server/api/0x/__tests__/0x-price.test.ts index dcffc23f..8333284c 100644 --- a/src/server/api/0x/__tests__/0x-price.test.ts +++ b/src/server/api/0x/__tests__/0x-price.test.ts @@ -22,6 +22,7 @@ import configTokens from '../../../config/config-tokens'; import { initTokens } from '../../../tokens/network-tokens'; import { initNetworkProviders } from '../../../providers/active-network-providers'; import { + formatPriceResponse, overrideZeroXPriceLookupDelay_TEST_ONLY, ZeroXPriceData, ZeroXPriceParams, @@ -52,6 +53,8 @@ const TOKEN_PRICE_SOURCE = TokenPriceSource.ZeroX; const expectedZeroXPriceOutput: ZeroXPriceData = { price: '1234.56', + minBuyAmount: '1000000000000', + sellAmount: '100000000000' }; const validatePriceRefresherOutput = (chain: BroadcasterChain) => { @@ -83,7 +86,7 @@ describe('0x-price', () => { resetTokenPriceCache(); const tokenConfigs = { - [TOKEN_ADDRESS_1]: getMockTokenConfig(), + [TOKEN_ADDRESS_1]: getMockTokenConfig('DAI'), [TOKEN_ADDRESS_2]: getMockTokenConfig(), }; @@ -100,18 +103,23 @@ describe('0x-price', () => { it('Should run live 0x API fetch for RAIL token', async () => { const params: ZeroXPriceParams = { - sellToken: '0xe76c6c83af64e4c60245d8c7de953df673a7a33d', // RAIL - buyToken: 'DAI', - sellAmount: '1000000000000000000', // 1 token + chainId: '1', + sellToken: configNetworks[0][1].gasToken.wrappedAddress, // '0xe76c6c83af64e4c60245d8c7de953df673a7a33d', // RAIL + buyToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + sellAmount: (1 * 10 ** 18).toString(), // '1000000000000000000', // 1 token } as ZeroXPriceParams; const zeroXPriceData = await getZeroXData( ZeroXApiEndpoint.PriceLookup, chainEthereum, params, ); + const formattedPrice = formatPriceResponse(zeroXPriceData); // BigInt(zeroXPriceData.minBuyAmount) / BigInt(zeroXPriceData.sellAmount) + expect(zeroXPriceData).to.be.an('object'); - expect(zeroXPriceData.price).to.be.a('string'); - expect(parseFloat(zeroXPriceData.price)).to.be.a('number'); + expect(zeroXPriceData.minBuyAmount).to.be.a('string'); + expect(zeroXPriceData.sellAmount).to.be.a('string'); + + expect(formattedPrice).to.be.a('string'); }).timeout(5000); it('Should format prices from mock ZeroX response', async () => { @@ -130,6 +138,7 @@ describe('0x-price', () => { tokenPrice, ), ); + validatePriceRefresherOutput(chainEthereum); stubGetZeroXData.restore(); diff --git a/src/server/config/fallback-providers/1-ethereum.ts b/src/server/config/fallback-providers/1-ethereum.ts index 908288ab..0ae84943 100644 --- a/src/server/config/fallback-providers/1-ethereum.ts +++ b/src/server/config/fallback-providers/1-ethereum.ts @@ -4,7 +4,7 @@ const config: FallbackProviderJsonConfig = { chainId: 1, providers: [ { - provider: 'https://mainnet.infura.io/v3/84842078b09946638c03157f83405213', + provider: 'https://eth.llamarpc.com', priority: 2, weight: 2, maxLogsPerBatch: 1, // Supports up to 10, but at 1 ethers handles getLogs differently, and this seems to be more stable. diff --git a/src/server/config/fallback-providers/137-polygon-pos.ts b/src/server/config/fallback-providers/137-polygon-pos.ts index 56dc52c8..ad84932a 100644 --- a/src/server/config/fallback-providers/137-polygon-pos.ts +++ b/src/server/config/fallback-providers/137-polygon-pos.ts @@ -4,7 +4,7 @@ const config: FallbackProviderJsonConfig = { chainId: 137, providers: [ { - provider: 'https://rpc.ankr.com/polygon', + provider: 'https://rpc-mainnet.matic.quiknode.pro', priority: 2, weight: 2, maxLogsPerBatch: 1, // Supports up to 10, but at 1 ethers handles getLogs differently, and this seems to be more stable. diff --git a/src/server/networking/waku-rest-api-client.ts b/src/server/networking/waku-rest-api-client.ts index 1afe59a9..404da8c6 100644 --- a/src/server/networking/waku-rest-api-client.ts +++ b/src/server/networking/waku-rest-api-client.ts @@ -149,7 +149,7 @@ export class WakuRestApiClient { const data = await this.request( WakuRequestMethods.DeleteSubscriptions, 'DELETE', - [topics], + topics, ); return data; } diff --git a/src/server/tokens/network-tokens.ts b/src/server/tokens/network-tokens.ts index 88942dfc..86c7ddb8 100644 --- a/src/server/tokens/network-tokens.ts +++ b/src/server/tokens/network-tokens.ts @@ -76,6 +76,21 @@ export const allTokenAddressesForNetwork = ( ); }; +export const tokenForSymbol = ( + chain: BroadcasterChain, + symbol: string +) =>{ + const tokens = networkTokens[chain.type][chain.id]; + for (const token of tokens) { + if (token.symbol.toLowerCase() === symbol.toLowerCase()) { + return token; + } + } + throw new Error( + `Unsupported token for chain ${chain.type}:${chain.id}: ${symbol}`, + ); +} + export const tokenForAddress = ( chain: BroadcasterChain, address: string, diff --git a/src/server/transactions/__tests__/transaction-validator.test.ts b/src/server/transactions/__tests__/transaction-validator.test.ts index 4824ff9a..f250a871 100644 --- a/src/server/transactions/__tests__/transaction-validator.test.ts +++ b/src/server/transactions/__tests__/transaction-validator.test.ts @@ -8,7 +8,7 @@ const { expect } = chai; describe('transaction-validator', () => { it('Should throw on bad transaction validation', () => { expect(() => - createValidTransaction('0x1234', '{{', BigInt('0x123')), + createValidTransaction({ id: 1, type: 0 },'0x1234', '{{', BigInt('0x123')), ).to.throw('Could not create valid transaction object.'); }); }); diff --git a/src/server/transactions/transaction-validator.ts b/src/server/transactions/transaction-validator.ts index 07d0d31c..c549994e 100644 --- a/src/server/transactions/transaction-validator.ts +++ b/src/server/transactions/transaction-validator.ts @@ -1,7 +1,11 @@ -import { ContractTransaction, getAddress, isHexString } from 'ethers'; +import { ContractTransaction, getAddress, Interface, isHexString, type InterfaceAbi, type TransactionDescription } from 'ethers'; import { logger } from '../../util/logger'; +import { ABI_RELAY_ADAPT } from '../abi/abi'; +import type { BroadcasterChain } from '../../models/chain-models'; +import configNetworks from '../config/config-networks'; export const createValidTransaction = ( + chain: BroadcasterChain, to: string, data: string, value?: bigint, @@ -10,14 +14,75 @@ export const createValidTransaction = ( if (!isHexString(data)) { throw new Error('Invalid data field.'); } + const isProxyCall = checkIsProxyAddress(chain, to); + const isRelayCall = checkIsRelayAdaptAddress(chain, to); + const validatedTo = isProxyCall || isRelayCall; + + if(!validatedTo){ + throw new Error("Invalid to address. Must be a known contract.") + } + + if(isRelayCall){ + const decoded = decodeData(data, ABI_RELAY_ADAPT); + if(decoded){ + const needsValidation = isCallToRelay(decoded); + if(needsValidation){ + const validatedData = validateRelayCallData(decoded); + if(!validatedData){ + throw new Error("Invalid Transaction Call.") + } + } + } + } const validTransaction: ContractTransaction = { to: getAddress(to), data, value, }; + return validTransaction; } catch (err) { logger.error(err); throw new Error('Could not create valid transaction object.'); } }; + +const decodeData = (data: string, abi: InterfaceAbi) => { + const iface = new Interface(abi); + try { + const decoded = iface.parseTransaction({ data }); + return decoded; + } catch { + return null; + } +}; + +const isCallToRelay = (decoded: TransactionDescription | null) =>{ + if (!decoded) return false; + if (decoded.name !== "relay") return false; + return true +} + +const validateRelayCallData = (decoded: TransactionDescription) => { + const invalid: boolean = decoded.args[1][1]; + return !invalid; +}; + +const checkIsProxyAddress = ( + chain: BroadcasterChain, + address: string, +) =>{ + const { proxyContract } = + configNetworks[chain.type][chain.id]; + return address.toLowerCase() === proxyContract.toLowerCase() + +} + +const checkIsRelayAdaptAddress = ( + chain: BroadcasterChain, + address: string, +) =>{ + const { relayAdaptContract } = + configNetworks[chain.type][chain.id]; + return address.toLowerCase() === relayAdaptContract.toLowerCase() +} diff --git a/src/server/waku-broadcaster/__tests__/waku-broadcaster.test.ts b/src/server/waku-broadcaster/__tests__/waku-broadcaster.test.ts index 8ba54020..8a48ee09 100644 --- a/src/server/waku-broadcaster/__tests__/waku-broadcaster.test.ts +++ b/src/server/waku-broadcaster/__tests__/waku-broadcaster.test.ts @@ -89,6 +89,8 @@ const handleHTTPPost = async (url: string, data?: unknown) => { return { data: {}, status: 200 }; }; +const MOCK_TO_ADDRESS = configNetworks[chain.type][chain.id].proxyContract; + describe('waku-broadcaster', () => { before(async () => { configDefaults.transactionFees.feeExpirationInMS = 5 * 60 * 1000; @@ -175,7 +177,7 @@ describe('waku-broadcaster', () => { 'string', 'No fee for token in broadcast data', ); - expect(BigInt(data.fees[MOCK_TOKEN_ADDRESS]).toString()).to.equal( + expect(BigInt(data.fees[MOCK_TOKEN_ADDRESS] as string).toString()).to.equal( '990424892220000000000', ); expect(data.feeExpiration).to.be.a('number'); @@ -255,7 +257,7 @@ describe('waku-broadcaster', () => { chainType: chain.type, feesID: '468abc', minGasPrice: '0x1000', - to: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE', + to: MOCK_TO_ADDRESS, data: '0x1234', broadcasterViewingKey: ByteUtils.hexlify(viewingPublicKey), useRelayAdapt: false, diff --git a/src/server/waku-broadcaster/methods/transact-method.ts b/src/server/waku-broadcaster/methods/transact-method.ts index 40fd7b84..06ea61d3 100644 --- a/src/server/waku-broadcaster/methods/transact-method.ts +++ b/src/server/waku-broadcaster/methods/transact-method.ts @@ -221,7 +221,7 @@ export const transactMethod = async ( ); } - const transaction = createValidTransaction(to, data, 0n); + const transaction = createValidTransaction(chain, to, data, 0n); const txResponse = await processTransaction( txidVersion, @@ -285,7 +285,7 @@ export const transactMethod = async ( dbg( `LOW FEE DETECTED: Retrying Transaction with minGasPrice: ${formattedInflatedSuggestion}`, ); - const transaction = createValidTransaction(to, data, 0n); + const transaction = createValidTransaction(chain, to, data, 0n); const txResponse = await processTransaction( txidVersion, chain, diff --git a/src/test/mocks.test.ts b/src/test/mocks.test.ts index 678b053a..48df5dd8 100644 --- a/src/test/mocks.test.ts +++ b/src/test/mocks.test.ts @@ -41,8 +41,10 @@ export const getMockNetwork = (): Network => { gasEstimateLimitToActualRatio: 1.25, profit: 0.07, }, - proxyContract: '0x00', - relayAdaptContract: '0x00', + // proxyContract: '0x00', + // relayAdaptContract: '0x00', + proxyContract: NETWORK_CONFIG[NetworkName.Ethereum].proxyContract, + relayAdaptContract: NETWORK_CONFIG[NetworkName.Ethereum].relayAdaptContract, coingeckoNetworkId: CoingeckoNetworkID.Ethereum, fallbackProviderConfig: fallbackProvidersEthereum, deploymentBlock: 100, @@ -104,9 +106,9 @@ export const getMockSerializedTransaction = (): string => { return JSON.stringify(getMockContractTransaction()); }; -export const getMockTokenConfig = (): TokenConfig => { +export const getMockTokenConfig = (symbol = 'SHIB'): TokenConfig => { return { - symbol: 'SHIB', + symbol, }; }; diff --git a/yarn.lock b/yarn.lock index eb2faae2..2802a9c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1353,10 +1353,10 @@ resolved "https://registry.yarnpkg.com/@railgun-community/curve25519-scalarmult-wasm/-/curve25519-scalarmult-wasm-0.1.5.tgz#edb5e40a8f1fdb7f7e0de1d04e8badabe41cb2a8" integrity sha512-Cur8uM/IdNW3WzUwdPloDG1CF0rpSBDWq2yGXJMZ7IMeBGdc4Jj0h9tnNMFRZ35ZuCVVbZ3yClmqSa4PsbpdGQ== -"@railgun-community/engine@9.3.1": - version "9.3.1" - resolved "https://registry.yarnpkg.com/@railgun-community/engine/-/engine-9.3.1.tgz#cfc14b6a7dd7d4c841e0fbe91ccf417e71414424" - integrity sha512-fTN/Ct+myrcecBZOlk8+pEdNZI0gy3s9aGeu54YKAn1TAMORH9udu9dQ3UFEkXS9P+EdcABSpbxPp/KIrp+N6Q== +"@railgun-community/engine@9.5.1": + version "9.5.1" + resolved "https://registry.yarnpkg.com/@railgun-community/engine/-/engine-9.5.1.tgz#ed761969c133ef85edc15900de7fdad96dbeeb37" + integrity sha512-tLX0WSHABzRC1VB7vhdwSBDFUYCFqRYHD2ek5/GT9+AZaKZJ6fU+UrAJYKxyEaGJ066l3M70fChc0Q6qM21scQ== dependencies: "@noble/ciphers" "^0.4.0" "@noble/ed25519" "^1.7.1" @@ -1388,15 +1388,15 @@ resolved "https://registry.yarnpkg.com/@railgun-community/poseidon-hash-wasm/-/poseidon-hash-wasm-1.0.1.tgz#bfd3b3f915391a59486d8c9d632069f029e152d6" integrity sha512-zDAbMu095ZjMqsKIoyOnuKR4Zofnqqx4eTCtnA6qYglnexX5cadxItvJa3ozhDDTmFkDGAidDssQ7Pzl3wwLDA== -"@railgun-community/shared-models@7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@railgun-community/shared-models/-/shared-models-7.5.0.tgz#d36d73740e32dbd5c21b2864e8c3db96e1241cb3" - integrity sha512-npSFFzSq2nSCLseEBZmLwJlZ4hMJArzEeM/DpqAAyxaTfznSHvpPIKmvEWorXVGjG+6xB7AY01TPGOq+3XHdcA== +"@railgun-community/shared-models@8.0.0", "@railgun-community/shared-models@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@railgun-community/shared-models/-/shared-models-8.0.0.tgz#689d4f6a3a80060e018abeb1b351cc711f97e77c" + integrity sha512-JY2HOobRlZpX9CsfiOfbjA2zmG3qLYA/QUupfz0V3ERUErQIm1ZQBSdMfVDZWhflW/4As+rbsY7vc3rXLGbpvA== -"@railgun-community/wallet@10.3.3": - version "10.3.3" - resolved "https://registry.yarnpkg.com/@railgun-community/wallet/-/wallet-10.3.3.tgz#cef885628b76b3c547a0016f298a2e5752fb7bbe" - integrity sha512-dsQV7sjso7mhE15HvJliLEMLsuvUMN5wDt6hSP7nm8S+n6hsBx6tSzc5JAI7nPsi1UVDIW/f96pqMRRPRPgYxw== +"@railgun-community/wallet@^10.8.1": + version "10.8.1" + resolved "https://registry.yarnpkg.com/@railgun-community/wallet/-/wallet-10.8.1.tgz#361c6a11e41b194dcc4878ef8a66848d0d8dfd24" + integrity sha512-UEVydOi6k2nWEXh54gsgTpX5/ncbiV/uqnkV0DQzinXSeGcCLKNEopcogSYT/1mcIjTwHEs+rR6iNc5IwlZdcA== dependencies: "@graphql-mesh/cache-localforage" "^0.7.20" "@graphql-mesh/cross-helpers" "^0.3.4" @@ -1409,8 +1409,8 @@ "@graphql-mesh/types" "^0.91.14" "@graphql-mesh/utils" "^0.43.22" "@noble/ed25519" "^1.7.1" - "@railgun-community/engine" "9.3.1" - "@railgun-community/shared-models" "7.5.0" + "@railgun-community/engine" "9.5.1" + "@railgun-community/shared-models" "8.0.0" "@whatwg-node/fetch" "^0.8.4" assert "2.0.0" axios "1.7.2" @@ -7289,7 +7289,16 @@ strict-uri-encode@^1.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" integrity sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7341,7 +7350,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7355,6 +7364,13 @@ strip-ansi@^5.0.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -8217,8 +8233,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8236,6 +8251,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"