Commit ed139974 authored by XFT's avatar XFT
Browse files

.

parent b2fbd6c6
Pipeline #26 failed with stages
in 0 seconds
KEY=
RPC=
PORT=
GAS_LIMIT=
REFUND=
PRIORITY_FEE=
CHAIN_ID=
FEE=
SHIFTERS=
ORACLE=
EOA_ONLY=
LIVE_FEE=
HOST=
LIMIT=
\ No newline at end of file
node_modules
package-lock.json
\ No newline at end of file
FROM node:16.15.0-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --production
COPY . .
EXPOSE 8080
CMD node main
\ No newline at end of file
# offshift-relayer-public
# Offshift Relayer Server
This relayer server is used to submit withdrawal transactions for the Offshift platform. The included configuration is compatible with Offshift's live deployment of Offshift Anon. You only need to provide your withdrawal account private key and RPC (and optionally, your fee).
Your chosen fee is denominated in aUSD Wei, deducted from the withdrawal transaction and added to your relayer's account. You will most likely want to keep `LIVE_FEE` enabled as that will charge the actual gas price on top of your set `FEE` value.
Once your relayer is live, it can be used with either the Offshift frontend or the CLI tool.
**The relayer has been tested using node v19+**
# Using the Docker container
To run your own relayer using a Docker image specified in the `Dockerfile`, follow these steps:
1. Install Docker on your machine. You can download Docker from the official website: https://www.docker.com/get-started
2. Clone the repository or download the `Dockerfile` and application files.
3. Navigate to the directory where the `Dockerfile` and application files are located.
4. Build the Docker image using the command:
```
docker build -t relayer .
```
This command builds a Docker image with the tag `relayer` based on the `Dockerfile` in the current directory.
You can provide any of the following environment variables as arguments before running your container:
* `KEY` is the private key of the relayer account (required)
* `RPC` provide your own RPC url (required)
* `PORT` select an outgoing port for your relayer
* `FEE` sets the fee for using your relayer (in aUSD Wei)
* `LIVE_FEE` will add the current cost of gas to your fee when set to `true`
5. Run the Docker container using the following command replacing 'key' and 'fee' with your relayer private key and your requested fee in wei:
```
docker run -p 8080:8080 \
-e KEY=key \
-e FEE=fee \
-e PORT=8080 \
relayer
```
This command runs the `relayer` Docker image in a container, maps port 8080 on the host machine to port 8080 in the container, using your set key and fee.
6. You can verify that the container is working properly by opening a web browser and navigating to `http://localhost:8080/config`.
If everything is working correctly, you should see the the fee set to your provided value, and the address associated with the private key you created the instance with.
# Run without Docker
To run your own instance without using the Docker image, follow these steps:
1. Clone the repository and open a terminal window at `/relayer`.
2. Make sure that you have node installed and are using node version 18 or higher.
3. Run `npm install` to install all of the dependencies.
4. You can then modify the `.env` file and run with `npm run start`
\ No newline at end of file
{
"key": "<PRIVATE KEY>",
"rpc": "<RPC URL>",
"port": 80,
"gasLimit": 400000,
"refund": "0",
"priorityFee": "2",
"chainId": "1",
"fee": "0",
"shifters": [
"0x207793a5a08e1da37b44c874a5006D32cF1d52C8",
"0x3704e2Af0e5a828549F9fF9da93eda10A7CCf402",
"0xd5e9156e77C88f1cC7A49291256adc643A163747"
],
"oracle": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419",
"eoaOnly": true,
"liveFee": true,
"host": "0.0.0.0",
"limit": 10
}
\ No newline at end of file
const config = require('../config.json');
require('dotenv').config()
const validKeys = [
"key",
"rpc",
"port",
"gasLimit",
"refund",
"priorityFee",
"chainId",
"fee",
"shifters",
"oracle",
"eoaOnly",
"liveFee",
"host",
"limit"
];
const camelToAllCaps = (str) => {
return str.replace(/[A-Z]/g, (match) => `_${match}`).toUpperCase();
}
validKeys.forEach(key => {
let envKey = camelToAllCaps(key)
if(process.env[envKey]){
if(key == "liveFee" || key == "eoaOnly" || key == "port" || key == "gasLimit" || key == "limit")
config[key] = JSON.parse(process.env[envKey]);
else
config[key] = process.env[envKey];
}
})
global.config = config;
global.config.initialFee = config.fee;
const configController = {
rateLimiter: async (req, res, next) => {
const currentTime = Date.now();
if (!req.app.locals.rateLimiter) req.app.locals.rateLimiter = {}
if (!req.app.locals.rateLimiter[req.ip]) {
req.app.locals.rateLimiter[req.ip] = {
requests: [currentTime],
lastChecked: currentTime,
};
}
const rateLimiter = req.app.locals.rateLimiter[req.ip];
rateLimiter.requests.push(currentTime);
if (currentTime - rateLimiter.lastChecked > 60000) {
rateLimiter.requests = rateLimiter.requests.filter(
(time) => currentTime - time < 60000
);
rateLimiter.lastChecked = currentTime;
}
if (rateLimiter.requests.length > config.limit) res.json({ error: "Rate limit exceeded" });
else next();
},
getConfig: async (relayController, res) => res.json({
refund: config.refund,
chainId: config.chainId,
fee: (BigInt(config.initialFee) + (config.liveFee ? BigInt(relayController.liveFee()) : BigInt("0"))).toString(),
address: relayController.address,
shifters: config.shifters
})
}
module.exports = { configController }
\ No newline at end of file
const Web3 = require('web3')
const web3 = new Web3(config.rpc);
const key = config.key;
let shifter;
const signer = web3.eth.accounts.privateKeyToAccount(config.key);
const refund = web3.utils.toWei(config.refund)
const validShifters = config.shifters.map(i => i.toLowerCase());
const shifterABI = require('../deps/Shifter.json').abi;
const shifters = validShifters.map(async (i) => await new web3.eth.Contract(shifterABI, i));
const oracle = new web3.eth.Contract(require('../deps/Chainlink.json').abi, config.oracle);
const relayController = {
load: async () => {
await web3.eth.accounts.wallet.add(signer);
relayController.address = signer.address;
console.log(`-> Loaded Account: ${relayController.address}`)
},
relay: async (req, res) => {
if (validShifters.includes(req.body.shifter.toLowerCase())) {
const args = [req.body.proof, ...Object.keys(req.body.args).map(e => req.body.args[e])];
const tx = await relayController.craft(args, req.body.shifter);
if (tx.error) res.json({ error: tx.error });
else res.json(tx);
} else res.json({ error: "Invalid shifter" });
},
craft: async (args, shifter) => {
shifter = new web3.eth.Contract(shifterABI, shifter);
const requiredFee = (BigInt(config.initialFee) + (config.liveFee ? BigInt(relayController.liveFee()) : BigInt("0"))) * 90n / 100n;
const paidFee = BigInt(args[5]);
if (paidFee < requiredFee) return ({ error: "Fee too low." });
config.eoaOnly && (await relayController.isEOA(args[3])) || ({ error: "Sender is not an EOA" })
let limit;
try {
limit = await shifter.methods.withdraw(...args).estimateGas({
from: signer.address,
to: shifter._address,
value: refund
});
} catch (e) {
console.log(e);
return ({ error: "Gas estimation failed" });
}
limit = limit + 50000;
const balance = await web3.eth.getBalance(signer.address);
if (balance < limit) ({ error: "Insufficient funds" });
const tx = {
to: shifter._address,
value: web3.utils.toWei(refund),
gas: limit.toString(),
data: shifter.methods.withdraw(...args).encodeABI()
};
signedTx = await web3.eth.accounts.signTransaction(tx, key)
web3.eth.sendSignedTransaction(signedTx.rawTransaction);
console.log(`Transaction sent - ${JSON.stringify(signedTx)}`);
return signedTx;
},
isEOA: async (address) => {
const code = await web3.eth.getCode(address);
return code === '0x';
},
updateLiveFee: async () => {
let usdPerEth = web3.utils.toBN(await oracle.methods.latestAnswer().call())
.mul(web3.utils.toBN(1e10));
let gasPrice = web3.utils.toBN(await web3.eth.getGasPrice());
let totalGasPrice = gasPrice.mul(web3.utils.toBN(config.gasLimit));
let requiredFee = usdPerEth
.mul(web3.utils.toBN(totalGasPrice))
.div(web3.utils.toBN(1e18));
config.fee = BigInt(requiredFee).toString();
},
liveFee: () => BigInt(config.fee).toString()
}
module.exports = { relayController }
\ No newline at end of file
{
"contractName": "ChainlinkOracle",
"abi": [
{
"inputs": [
{
"internalType": "address",
"name": "_aggregator",
"type": "address"
},
{
"internalType": "address",
"name": "_accessController",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "int256",
"name": "current",
"type": "int256"
},
{
"indexed": true,
"internalType": "uint256",
"name": "roundId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "updatedAt",
"type": "uint256"
}
],
"name": "AnswerUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "roundId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "startedBy",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "startedAt",
"type": "uint256"
}
],
"name": "NewRound",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "OwnershipTransferRequested",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "accessController",
"outputs": [
{
"internalType": "contract AccessControllerInterface",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "aggregator",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_aggregator",
"type": "address"
}
],
"name": "confirmAggregator",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "description",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_roundId",
"type": "uint256"
}
],
"name": "getAnswer",
"outputs": [
{
"internalType": "int256",
"name": "",
"type": "int256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint80",
"name": "_roundId",
"type": "uint80"
}
],
"name": "getRoundData",
"outputs": [
{
"internalType": "uint80",
"name": "roundId",
"type": "uint80"
},
{
"internalType": "int256",
"name": "answer",
"type": "int256"
},
{
"internalType": "uint256",
"name": "startedAt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "updatedAt",
"type": "uint256"
},
{
"internalType": "uint80",
"name": "answeredInRound",
"type": "uint80"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_roundId",
"type": "uint256"
}
],
"name": "getTimestamp",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "latestAnswer",
"outputs": [
{
"internalType": "int256",
"name": "",
"type": "int256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "latestRound",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "latestRoundData",
"outputs": [
{
"internalType": "uint80",
"name": "roundId",
"type": "uint80"
},
{
"internalType": "int256",
"name": "answer",
"type": "int256"
},
{
"internalType": "uint256",
"name": "startedAt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "updatedAt",
"type": "uint256"
},
{
"internalType": "uint80",
"name": "answeredInRound",
"type": "uint80"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "latestTimestamp",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address payable",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"name": "phaseAggregators",
"outputs": [
{
"internalType": "contract AggregatorV2V3Interface",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "phaseId",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_aggregator",
"type": "address"
}
],
"name": "proposeAggregator",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "proposedAggregator",
"outputs": [
{
"internalType": "contract AggregatorV2V3Interface",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint80",
"name": "_roundId",
"type": "uint80"
}
],
"name": "proposedGetRoundData",
"outputs": [
{
"internalType": "uint80",
"name": "roundId",
"type": "uint80"
},
{
"internalType": "int256",
"name": "answer",
"type": "int256"
},
{
"internalType": "uint256",
"name": "startedAt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "updatedAt",
"type": "uint256"
},
{
"internalType": "uint80",
"name": "answeredInRound",
"type": "uint80"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "proposedLatestRoundData",
"outputs": [
{
"internalType": "uint80",
"name": "roundId",
"type": "uint80"
},
{
"internalType": "int256",
"name": "answer",
"type": "int256"
},
{
"internalType": "uint256",
"name": "startedAt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "updatedAt",
"type": "uint256"
},
{
"internalType": "uint80",
"name": "answeredInRound",
"type": "uint80"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_accessController",
"type": "address"
}
],
"name": "setController",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_to",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]
}
\ No newline at end of file
This diff is collapsed.
const express = require("express");
const bodyParser = require('body-parser');
var cors = require('cors')
const app = express();
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
console.log(`Starting OffShift Relay `)
console.log(`-> Loading Config Controller`);
const { configController } = require('./controllers/configController.js');
console.log(config);
console.log(`-> Loading Relay Controller`);
const { relayController } = require('./controllers/relayController.js');
app.use(configController.rateLimiter);
app.get('/config', (req, res) => configController.getConfig(relayController, res));
app.post('/relay', relayController.relay);
app.listen(config.port, config.host, async () => {
await relayController.load();
if (config.liveFee) {
await relayController.updateLiveFee();
console.log(`-> Live Fee Enabled. Current fee (aUSD in wei): ${config.fee}`);
setInterval(async () => await relayController.updateLiveFee(), 60000);
}
console.log(`-> Config:\n--> ${JSON.stringify({
refund: config.refund,
chainId: config.chainId,
fee: parseInt(config.fee).toString(),
address: relayController.address,
shifters: config.shifters
})}`);
console.log(`-> Relay started at http://${config.host}:${config.port}`);
});
process.on('unhandledRejection', (reason, promise) => {
console.log(`${new Date} ${reason}`);
});
\ No newline at end of file
{
"name": "relayer",
"version": "1.0.0",
"description": "Relayer system for OffshiftAnon.",
"main": "main.js",
"scripts": {
"start": "node main",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.0",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.1",
"fs": "^0.0.1-security",
"web3": "^1.7.5"
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment