diff --git a/.env.example b/.env.example index 0e5daf4..a6f88d4 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ NET_ID=1 HTTP_RPC_URL=https://api.securerpc.com/v1 -# WS_RPC_URL=wss://mainnet.infura.io/ws/v3/ +WS_RPC_URL=wss://mainnet.infura.io/ws/v3/ # ORACLE_RPC_URL should always point to the mainnet ORACLE_RPC_URL=https://api.securerpc.com/v1 REDIS_URL=redis://127.0.0.1:6379 @@ -13,7 +13,7 @@ APP_PORT=8000 # without 0x prefix PRIVATE_KEY= # 0.05 means 0.05% -REGULAR_TORNADO_WITHDRAW_FEE=0.35 +RELAYER_FEE=0.4 MINING_SERVICE_FEE=0.05 REWARD_ACCOUNT= CONFIRMATIONS=4 diff --git a/abis/OffchainOracle.abi.json b/abis/OffchainOracle.abi.json deleted file mode 100644 index 57b2d2a..0000000 --- a/abis/OffchainOracle.abi.json +++ /dev/null @@ -1,181 +0,0 @@ -[ - { - "inputs": [ - { "internalType": "contract MultiWrapper", "name": "_multiWrapper", "type": "address" }, - { "internalType": "contract IOracle[]", "name": "existingOracles", "type": "address[]" }, - { "internalType": "enum OffchainOracle.OracleType[]", "name": "oracleTypes", "type": "uint8[]" }, - { "internalType": "contract IERC20[]", "name": "existingConnectors", "type": "address[]" }, - { "internalType": "contract IERC20", "name": "wBase", "type": "address" } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "contract IERC20", "name": "connector", "type": "address" } - ], - "name": "ConnectorAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "contract IERC20", "name": "connector", "type": "address" } - ], - "name": "ConnectorRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "contract MultiWrapper", "name": "multiWrapper", "type": "address" } - ], - "name": "MultiWrapperUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "contract IOracle", "name": "oracle", "type": "address" }, - { - "indexed": false, - "internalType": "enum OffchainOracle.OracleType", - "name": "oracleType", - "type": "uint8" - } - ], - "name": "OracleAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "contract IOracle", "name": "oracle", "type": "address" }, - { - "indexed": false, - "internalType": "enum OffchainOracle.OracleType", - "name": "oracleType", - "type": "uint8" - } - ], - "name": "OracleRemoved", - "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" - }, - { - "inputs": [{ "internalType": "contract IERC20", "name": "connector", "type": "address" }], - "name": "addConnector", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract IOracle", "name": "oracle", "type": "address" }, - { "internalType": "enum OffchainOracle.OracleType", "name": "oracleKind", "type": "uint8" } - ], - "name": "addOracle", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "connectors", - "outputs": [{ "internalType": "contract IERC20[]", "name": "allConnectors", "type": "address[]" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract IERC20", "name": "srcToken", "type": "address" }, - { "internalType": "contract IERC20", "name": "dstToken", "type": "address" }, - { "internalType": "bool", "name": "useWrappers", "type": "bool" } - ], - "name": "getRate", - "outputs": [{ "internalType": "uint256", "name": "weightedRate", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract IERC20", "name": "srcToken", "type": "address" }, - { "internalType": "bool", "name": "useSrcWrappers", "type": "bool" } - ], - "name": "getRateToEth", - "outputs": [{ "internalType": "uint256", "name": "weightedRate", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "multiWrapper", - "outputs": [{ "internalType": "contract MultiWrapper", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "oracles", - "outputs": [ - { "internalType": "contract IOracle[]", "name": "allOracles", "type": "address[]" }, - { "internalType": "enum OffchainOracle.OracleType[]", "name": "oracleTypes", "type": "uint8[]" } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "contract IERC20", "name": "connector", "type": "address" }], - "name": "removeConnector", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract IOracle", "name": "oracle", "type": "address" }, - { "internalType": "enum OffchainOracle.OracleType", "name": "oracleKind", "type": "uint8" } - ], - "name": "removeOracle", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "contract MultiWrapper", "name": "_multiWrapper", "type": "address" }], - "name": "setMultiWrapper", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } -] diff --git a/package.json b/package.json index 967ec33..61d0879 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "relay", - "version": "5.1.0", - "description": "Relayer for Tornado.cash privacy solution. https://tornado.cash", + "version": "5.2.0", + "description": "Relayer for Tornado.cash privacy solution.", "scripts": { "server": "node src/server.js", "worker": "node src/worker", @@ -16,22 +16,22 @@ "build": "docker build -t tornadocash/relayer:mainnet-v5 .", "start": "docker-compose up -d redis && concurrently \"yarn server\" \"yarn priceWatcher\" \"yarn treeWatcher\" \"yarn worker\" \"yarn healthWatcher\"" }, - "author": "tornado.cash", + "author": "Tornado Cash team", "license": "MIT", "dependencies": { - "circomlib": "git+https://git.tornado.ws/tornado-packages/circomlib.git#3b492f9801573eebcfe1b6c584afe8a3beecf2b4", + "@tornado/tornado-config": "^2.0.0", + "@tornado/tornado-oracles": "^2.1.0", "ajv": "^6.12.5", "async-mutex": "^0.2.4", "bull": "^3.12.1", + "circomlib": "git+https://git.tornado.ws/tornado-packages/circomlib.git#3b492f9801573eebcfe1b6c584afe8a3beecf2b4", "concurrently": "^8.2.0", "dotenv": "^8.2.0", "eth-ens-namehash": "^2.0.8", "express": "^4.17.1", "fixed-merkle-tree": "^0.4.0", - "@tornado/gas-price-oracle": "^0.5.3", "ioredis": "^4.14.1", "node-fetch": "^2.6.7", - "torn-token": "1.0.6", "tornado-anonymity-mining": "^2.1.2", "tx-manager": "^0.4.8", "uuid": "^8.3.0", diff --git a/src/config.js b/src/config.js index f600856..78a65a1 100644 --- a/src/config.js +++ b/src/config.js @@ -1,14 +1,13 @@ require('dotenv').config() const { jobType } = require('./constants') -const tornConfig = require('torn-token') +const tornConfig = require('@tornado/tornado-config') module.exports = { netId: Number(process.env.NET_ID) || 1, redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379', httpRpcUrl: process.env.HTTP_RPC_URL, wsRpcUrl: process.env.WS_RPC_URL, - oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://mainnet.infura.io/', - offchainOracleAddress: '0x07D91f5fb9Bf7798734C3f606dB065549F6893bb', + oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://api.securerpc.com/v1', aggregatorAddress: process.env.AGGREGATOR, minerMerkleTreeHeight: 20, privateKey: process.env.PRIVATE_KEY, @@ -16,13 +15,10 @@ module.exports = { torn: tornConfig, port: process.env.APP_PORT || 8000, tornadoServiceFee: Number(process.env.REGULAR_TORNADO_WITHDRAW_FEE), - miningServiceFee: Number(process.env.MINING_SERVICE_FEE), rewardAccount: process.env.REWARD_ACCOUNT, governanceAddress: '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce', tornadoGoerliProxy: '0x454d870a72e29d5E5697f635128D18077BD04C60', gasLimits: { - [jobType.TORNADO_WITHDRAW]: 390000, - WITHDRAW_WITH_EXTRA: 700000, [jobType.MINING_REWARD]: 455000, [jobType.MINING_WITHDRAW]: 400000, }, diff --git a/src/priceWatcher.js b/src/priceWatcher.js index 04eb0b7..3d4d500 100644 --- a/src/priceWatcher.js +++ b/src/priceWatcher.js @@ -1,41 +1,17 @@ -const { offchainOracleAddress } = require('./config') -const { - getArgsForOracle, - setSafeInterval, - toChecksumAddress, - toBN, - RelayerError, - logRelayerError, -} = require('./utils') +const { setSafeInterval, RelayerError, logRelayerError } = require('./utils') const { redis } = require('./modules/redis') -const web3 = require('./modules/web3')('oracle') +const { TokenPriceOracle } = require('@tornado/tornado-oracles') +const { oracleRpcUrl } = require('./config') -const offchainOracleABI = require('../abis/OffchainOracle.abi.json') - -const offchainOracle = new web3.eth.Contract(offchainOracleABI, offchainOracleAddress) -const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle() +const priceOracle = new TokenPriceOracle(oracleRpcUrl) async function main() { try { - const ethPrices = {} - for (let i = 0; i < tokenAddresses.length; i++) { - try { - const isWrap = - toChecksumAddress(tokenAddresses[i]) === - toChecksumAddress('0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643') - - const price = await offchainOracle.methods.getRateToEth(tokenAddresses[i], isWrap).call() - const numerator = toBN(oneUintAmount[i]) - const denominator = toBN(10).pow(toBN(18)) // eth decimals - const priceFormatted = toBN(price).mul(numerator).div(denominator) - ethPrices[currencyLookup[tokenAddresses[i]]] = priceFormatted.toString() - } catch (e) { - console.error('cant get price of ', tokenAddresses[i]) - } - } + const ethPrices = await priceOracle.fetchPrices() if (!Object.values(ethPrices).length) { throw new RelayerError('Can`t update prices', 1) } + await redis.hmset('prices', ethPrices) console.log('Wrote following prices to redis', ethPrices) } catch (e) { diff --git a/src/utils.js b/src/utils.js index 98f309e..c771d95 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,16 +2,8 @@ const { instances, netId } = require('./config') const { poseidon } = require('circomlib') const { toBN, toChecksumAddress, BN, fromWei, isAddress, toWei, toHex } = require('web3-utils') -const TOKENS = { - torn: { - tokenAddress: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C', - symbol: 'TORN', - decimals: 18, - }, -} - const addressMap = new Map() -const instance = instances[`netId${netId}`] +const instance = instances[netId] for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(instance)) { Object.entries(instanceAddress).forEach(([amount, address]) => @@ -61,24 +53,6 @@ function when(source, event) { }) } -function getArgsForOracle() { - const tokens = { - ...instances.netId1, - ...TOKENS, - } - const tokenAddresses = [] - const oneUintAmount = [] - const currencyLookup = {} - Object.entries(tokens).map(([currency, data]) => { - if (currency !== 'eth') { - tokenAddresses.push(data.tokenAddress) - oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString()) - currencyLookup[data.tokenAddress] = currency - } - }) - return { tokenAddresses, oneUintAmount, currencyLookup } -} - function fromDecimals(value, decimals) { value = value.toString() let ether = value.toString() @@ -155,7 +129,6 @@ module.exports = { poseidonHash2, sleep, when, - getArgsForOracle, fromDecimals, toBN, toChecksumAddress, diff --git a/src/worker.js b/src/worker.js index 6f4899b..4421c5b 100644 --- a/src/worker.js +++ b/src/worker.js @@ -1,8 +1,7 @@ const fs = require('fs') const MerkleTree = require('fixed-merkle-tree') -const { GasPriceOracle } = require('@tornado/gas-price-oracle') const { Utils, Controller } = require('tornado-anonymity-mining') - +const { TornadoFeeOracleV5 } = require('@tornado/tornado-oracles') const swapABI = require('../abis/swap.abi.json') const miningABI = require('../abis/mining.abi.json') const tornadoABI = require('../abis/tornadoABI.json') @@ -47,7 +46,7 @@ let txManager let controller let swap let minerContract -let gasPriceOracle +const feeOracle = new TornadoFeeOracleV5(netId, oracleRpcUrl) async function fetchTree() { const elements = await redis.get('tree:elements') @@ -94,12 +93,7 @@ async function start() { BASE_FEE_RESERVE_PERCENTAGE: baseFeeReserve, }, }) - gasPriceOracle = new GasPriceOracle({ - defaultRpc: oracleRpcUrl, - minPriority: 2, - percentile: 5, - blocksCount: 20, - }) + swap = new web3.eth.Contract(swapABI, await resolver.resolve(torn.rewardSwap.address)) minerContract = new web3.eth.Contract(miningABI, await resolver.resolve(torn.miningV2.address)) redisSubscribe.subscribe('treeUpdate', fetchTree) @@ -125,37 +119,6 @@ function checkFee({ data }, gasInfo) { return checkMiningFee(data) } -async function getGasPrice() { - try { - const { maxFeePerGas, gasPrice } = await gasPriceOracle.getTxGasParams({ - legacySpeed: 'fast', - bumpPercent: 10, - }) - - return toBN(maxFeePerGas || gasPrice) - } catch (e) { - const block = await web3.eth.getBlock('latest') - - if (block && block.baseFeePerGas) { - return toBN(block.baseFeePerGas) - } - - const gasPrice = await web3.eth.getGasPrice() - return toBN(gasPrice) - } -} - -async function estimateWithdrawalGasLimit(tx) { - try { - const fetchedGasLimit = await web3.eth.estimateGas(tx) - const bumped = Math.floor(fetchedGasLimit * 1.2) - return bumped - } catch (e) { - console.log('Estimation error: ', e) - return gasLimits[jobType.TORNADO_WITHDRAW] - } -} - async function checkTornadoFee({ args, contract }, { gasLimit, gasPrice }) { const { currency, amount, decimals } = getInstance(contract) const [fee, refund] = [args[4], args[5]].map(toBN) @@ -197,12 +160,12 @@ async function checkTornadoFee({ args, contract }, { gasLimit, gasPrice }) { } async function checkMiningFee({ args }) { - const gasPrice = await getGasPrice() + const gasPrice = await feeOracle.getGasPrice() const ethPrice = await redis.hget('prices', 'torn') const isMiningReward = currentJob.data.type === jobType.MINING_REWARD const providedFee = isMiningReward ? toBN(args.fee) : toBN(args.extData.fee) - const expense = gasPrice.mul(toBN(gasLimits[currentJob.data.type])) + const expense = toBN(gasPrice).mul(toBN(gasLimits[currentJob.data.type])) const expenseInTorn = expense.mul(toBN(1e18)).div(toBN(ethPrice)) // todo make aggregator for ethPrices and rewardSwap data const balance = await swap.methods.tornVirtualBalance().call() @@ -259,17 +222,15 @@ async function getTxObject({ data }) { calldata = contract.methods.withdraw(data.proof, ...data.args).encodeABI() } - const gasPrice = await getGasPrice() const incompleteTx = { value: data.args[5], from: txManager.address, // Required, because without it relayerRegistry.burn will fail, because msg.sender is not relayer to: contract._address, data: calldata, - gasPrice: toHex(gasPrice), } - const gasLimit = await estimateWithdrawalGasLimit(incompleteTx) + const { gasLimit, gasPrice } = await feeOracle.getGasParams(incompleteTx, 'relayer_withdrawal') - return Object.assign(incompleteTx, { gasLimit }) + return Object.assign(incompleteTx, { gasLimit, gasPrice }) } else { const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw' const calldata = minerContract.methods[method](data.proof, data.args).encodeABI() diff --git a/yarn.lock b/yarn.lock index b76c856..5f0db33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -409,7 +409,7 @@ fastfile "0.0.19" ffjavascript "^0.2.30" -"@openzeppelin/contracts@^3.1.0", "@openzeppelin/contracts@^3.4.0": +"@openzeppelin/contracts@^3.4.0": version "3.4.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== @@ -456,6 +456,22 @@ bignumber.js "^9.0.0" node-cache "^5.1.2" +"@tornado/tornado-config@^2.0.0": + version "2.0.0" + resolved "https://git.tornado.ws/api/packages/tornado-packages/npm/%40tornado%2Ftornado-config/-/2.0.0/tornado-config-2.0.0.tgz#52bbc179ecb2385f71b4d56e060b68e7dd6fb8b4" + integrity sha512-7EkpWNfEm34VEOrbLnPpvd/aUJYnA1L+6/qx2fZ/AfmuJFkjSZ18Z4jvVGNY7ktKIhTu3/Tbze+9l3eNueCNIA== + +"@tornado/tornado-oracles@^2.1.0": + version "2.1.0" + resolved "https://git.tornado.ws/api/packages/tornado-packages/npm/%40tornado%2Ftornado-oracles/-/2.1.0/tornado-oracles-2.1.0.tgz#2aa0d8c9288992e6d194d4bb28acb37c2035c453" + integrity sha512-Y6FPAGnCvHLWzUnNYgGoOv+X7KY3CF02rRSawataYaLyl+v2ivh7RYZZZ3G/B5hXf+pD3IFeCdm4PDnTNyNe1g== + dependencies: + "@tornado/gas-price-oracle" "^0.5.3" + "@tornado/tornado-config" "^2.0.0" + "@types/node" "^20.5.1" + bignumber.js "^9.1.1" + ethers "5.7" + "@types/bn.js@^4.11.3": version "4.11.6" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c" @@ -502,6 +518,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/node@^20.5.1": + version "20.5.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.8.tgz#fb171fd22d37ca6e2ea97fde88e6a13ee14bc327" + integrity sha512-eajsR9aeljqNhK028VG0Wuw+OaY5LLxYmxeoXynIoE6jannr9/Ucd1LL0hSSoafk5LTYG+FfqsyGt81Q6Zkybw== + "@types/pbkdf2@^3.0.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@types/pbkdf2/-/pbkdf2-3.1.0.tgz#039a0e9b67da0cdc4ee5dab865caa6b267bb66b1" @@ -788,6 +809,11 @@ bignumber.js@^9.0.0: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== +bignumber.js@^9.1.1: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -2098,7 +2124,7 @@ ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.1, ethereum ethereum-cryptography "^0.1.3" rlp "^2.2.4" -ethers@^5.4.6: +ethers@5.7, ethers@^5.4.6: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -4896,16 +4922,6 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -torn-token@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/torn-token/-/torn-token-1.0.6.tgz#66cde5f85b611033918c807b4a8d9d4e5bb3fcfc" - integrity sha512-ilCS7fN+JM2O8l1Iw5cEWXyiQQg8GxEeYYvqALJcn5cO6qSpD+xJb3Dji4EHXa1Yu1OBd/19ktWNvUkWNvuAaQ== - dependencies: - "@openzeppelin/contracts" "^3.1.0" - eth-sig-util "^2.5.3" - ethereumjs-util "^7.0.3" - web3 "^1.2.11" - tornado-anonymity-mining@^2.1.2: version "2.1.5" resolved "https://registry.yarnpkg.com/tornado-anonymity-mining/-/tornado-anonymity-mining-2.1.5.tgz#7dbc7f099ce9667f2cc91fbb7ce8f78663afc4cc"