Repo is ready
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# Poly-Merger
|
||||
|
||||
A utility for merging Polymarket positions efficiently. This tool helps in consolidating opposite positions in the same market, allowing you to:
|
||||
|
||||
1. Reduce gas costs
|
||||
2. Free up capital
|
||||
3. Simplify position management
|
||||
|
||||
## How It Works
|
||||
|
||||
The merger tool interacts with Polymarket's smart contracts to combine opposite positions in binary markets. When you hold both YES and NO shares in the same market, this tool will merge them to recover your USDC.
|
||||
|
||||
## Usage
|
||||
|
||||
The merger is invoked through the main Poly-Maker bot when position merging conditions are met, but you can also use it independently:
|
||||
|
||||
```
|
||||
node merge.js [amount_to_merge] [condition_id] [is_neg_risk_market]
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
node merge.js 1000000 1234567 true
|
||||
```
|
||||
|
||||
This would merge 1 USDC worth of opposing positions in market 1234567, which is a negative risk market.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js
|
||||
- ethers.js v5.x
|
||||
- A .env file with your Polygon network private key
|
||||
|
||||
## Notes
|
||||
|
||||
This implementation is based on open-source Polymarket code but has been optimized for automated market making operations.
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Poly-Merger: Position Merging Utility for Polymarket
|
||||
*
|
||||
* This script handles merging of YES and NO positions in Polymarket prediction markets
|
||||
* to recover collateral. It works with both regular and negative risk markets.
|
||||
*
|
||||
* The merger supports Gnosis Safe wallets through the safe-helpers.js utility.
|
||||
*
|
||||
* Usage:
|
||||
* node merge.js [amountToMerge] [conditionId] [isNegRiskMarket]
|
||||
*
|
||||
* Example:
|
||||
* node merge.js 1000000 12345 true
|
||||
*/
|
||||
|
||||
const { ethers } = require('ethers');
|
||||
const { resolve } = require('path');
|
||||
const { signAndExecuteSafeTransaction } = require('./safe-helpers');
|
||||
const { safeAbi } = require('./safeAbi');
|
||||
|
||||
// Load environment variables
|
||||
require('dotenv').config()
|
||||
|
||||
// Connect to Polygon network
|
||||
const provider = new ethers.providers.JsonRpcProvider("https://polygon.llamarpc.com");
|
||||
const privateKey = process.env.PK;
|
||||
const wallet = new ethers.Wallet(privateKey, provider);
|
||||
|
||||
// Polymarket contract addresses
|
||||
const addresses = {
|
||||
// Adapter contract for negative risk markets
|
||||
neg_risk_adapter: '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296',
|
||||
// USDC token contract on Polygon
|
||||
collateral: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
|
||||
// Main conditional tokens contract for prediction markets
|
||||
conditional_tokens: '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'
|
||||
};
|
||||
|
||||
// Minimal ABIs for the contracts we interact with
|
||||
const negRiskAdapterAbi = [
|
||||
"function mergePositions(bytes32 conditionId, uint256 amount)"
|
||||
];
|
||||
|
||||
const conditionalTokensAbi = [
|
||||
"function mergePositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] partition, uint256 amount)"
|
||||
];
|
||||
|
||||
/**
|
||||
* Merges YES and NO positions in a Polymarket prediction market to recover USDC collateral.
|
||||
*
|
||||
* This function handles both regular and negative risk markets via different contract calls.
|
||||
* It uses the Gnosis Safe wallet infrastructure for secure transaction execution.
|
||||
*
|
||||
* @param {string|number} amountToMerge - Raw amount of tokens to merge (typically expressed in raw units, e.g., 1000000 = 1 USDC)
|
||||
* @param {string|number} conditionId - The market's condition ID
|
||||
* @param {boolean} isNegRiskMarket - Whether this is a negative risk market (uses different contract)
|
||||
* @returns {string} The transaction hash of the merge operation
|
||||
*/
|
||||
async function mergePositions(amountToMerge, conditionId, isNegRiskMarket) {
|
||||
// Log parameters for debugging
|
||||
console.log(amountToMerge, conditionId, isNegRiskMarket);
|
||||
|
||||
// Prepare transaction parameters
|
||||
const nonce = await provider.getTransactionCount(wallet.address);
|
||||
const gasPrice = await provider.getGasPrice();
|
||||
const gasLimit = 10000000; // Set high gas limit to ensure transaction completes
|
||||
|
||||
let tx;
|
||||
// Different contract calls for different market types
|
||||
if (isNegRiskMarket) {
|
||||
// For negative risk markets, use the adapter contract
|
||||
const negRiskAdapter = new ethers.Contract(addresses.neg_risk_adapter, negRiskAdapterAbi, wallet);
|
||||
tx = await negRiskAdapter.populateTransaction.mergePositions(conditionId, amountToMerge);
|
||||
} else {
|
||||
// For regular markets, use the conditional tokens contract directly
|
||||
const conditionalTokens = new ethers.Contract(addresses.conditional_tokens, conditionalTokensAbi, wallet);
|
||||
tx = await conditionalTokens.populateTransaction.mergePositions(
|
||||
addresses.collateral, // USDC contract
|
||||
ethers.constants.HashZero, // Parent collection ID (0 for top-level markets)
|
||||
conditionId, // Market ID
|
||||
[1, 2], // Partition (indexes of outcomes to merge)
|
||||
amountToMerge // Amount to merge
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare full transaction object
|
||||
const transaction = {
|
||||
...tx,
|
||||
chainId: 137, // Polygon chain ID
|
||||
gasPrice: gasPrice,
|
||||
gasLimit: gasLimit,
|
||||
nonce: nonce
|
||||
};
|
||||
|
||||
// Get the Safe address from environment variables
|
||||
const safeAddress = process.env.BROWSER_ADDRESS;
|
||||
const safe = new ethers.Contract(safeAddress, safeAbi, wallet);
|
||||
|
||||
// Execute the transaction through the Safe
|
||||
console.log("Signing Transaction")
|
||||
const txResponse = await signAndExecuteSafeTransaction(
|
||||
wallet,
|
||||
safe,
|
||||
transaction.to,
|
||||
transaction.data,
|
||||
{
|
||||
gasPrice: transaction.gasPrice,
|
||||
gasLimit: transaction.gasLimit
|
||||
}
|
||||
);
|
||||
|
||||
console.log("Sent transaction. Waiting for response")
|
||||
const txReceipt = await txResponse.wait();
|
||||
|
||||
console.log("merge positions " + txReceipt.transactionHash);
|
||||
return txReceipt.transactionHash;
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Amount of tokens to merge (in raw units, e.g., 1000000 = 1 USDC)
|
||||
const amountToMerge = args[0];
|
||||
|
||||
// The market's condition ID
|
||||
const conditionId = args[1];
|
||||
|
||||
// Whether this is a negative risk market (true/false)
|
||||
const isNegRiskMarket = args[2] === 'true';
|
||||
|
||||
// Execute the merge operation and handle any errors
|
||||
mergePositions(amountToMerge, conditionId, isNegRiskMarket)
|
||||
.catch(error => {
|
||||
console.error("Error merging positions:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Generated
+1339
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "poly-merger",
|
||||
"version": "1.0.0",
|
||||
"description": "Position merging utility for Polymarket",
|
||||
"main": "merge.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"ethers": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
const { BigNumber, ethers } = require('ethers');
|
||||
|
||||
function joinHexData(hexData) {
|
||||
return `0x${hexData
|
||||
.map(hex => {
|
||||
const stripped = hex.replace(/^0x/, "");
|
||||
return stripped.length % 2 === 0 ? stripped : "0" + stripped;
|
||||
})
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
function abiEncodePacked(...params) {
|
||||
return joinHexData(
|
||||
params.map(({ type, value }) => {
|
||||
const encoded = ethers.utils.defaultAbiCoder.encode([type], [value]);
|
||||
|
||||
if (type === "bytes" || type === "string") {
|
||||
const bytesLength = parseInt(encoded.slice(66, 130), 16);
|
||||
return encoded.slice(130, 130 + 2 * bytesLength);
|
||||
}
|
||||
|
||||
let typeMatch = type.match(/^(?:u?int\d*|bytes\d+|address)\[\]$/);
|
||||
if (typeMatch) {
|
||||
return encoded.slice(130);
|
||||
}
|
||||
|
||||
if (type.startsWith("bytes")) {
|
||||
const bytesLength = parseInt(type.slice(5));
|
||||
return encoded.slice(2, 2 + 2 * bytesLength);
|
||||
}
|
||||
|
||||
typeMatch = type.match(/^u?int(\d*)$/);
|
||||
if (typeMatch) {
|
||||
if (typeMatch[1] !== "") {
|
||||
const bytesLength = parseInt(typeMatch[1]) / 8;
|
||||
return encoded.slice(-2 * bytesLength);
|
||||
}
|
||||
return encoded.slice(-64);
|
||||
}
|
||||
|
||||
if (type === "address") {
|
||||
return encoded.slice(-40);
|
||||
}
|
||||
|
||||
throw new Error(`unsupported type ${type}`);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function signTransactionHash(signer, message) {
|
||||
const messageArray = ethers.utils.arrayify(message);
|
||||
let sig = await signer.signMessage(messageArray);
|
||||
let sigV = parseInt(sig.slice(-2), 16);
|
||||
|
||||
switch (sigV) {
|
||||
case 0:
|
||||
case 1:
|
||||
sigV += 31;
|
||||
break;
|
||||
case 27:
|
||||
case 28:
|
||||
sigV += 4;
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid signature");
|
||||
}
|
||||
|
||||
sig = sig.slice(0, -2) + sigV.toString(16);
|
||||
|
||||
return {
|
||||
r: BigNumber.from("0x" + sig.slice(2, 66)).toString(),
|
||||
s: BigNumber.from("0x" + sig.slice(66, 130)).toString(),
|
||||
v: BigNumber.from("0x" + sig.slice(130, 132)).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function signAndExecuteSafeTransaction(signer, safe, to, data, overrides = {}) {
|
||||
const nonce = await safe.nonce();
|
||||
console.log("Nonce for safe: ", nonce);
|
||||
const value = "0";
|
||||
const safeTxGas = "0";
|
||||
const baseGas = "0";
|
||||
const gasPrice = "0";
|
||||
const gasToken = ethers.constants.AddressZero;
|
||||
const refundReceiver = ethers.constants.AddressZero;
|
||||
const operation = 0;
|
||||
|
||||
const txHash = await safe.getTransactionHash(
|
||||
to,
|
||||
value,
|
||||
data,
|
||||
operation,
|
||||
safeTxGas,
|
||||
baseGas,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
nonce
|
||||
);
|
||||
console.log("Transaction hash: ", txHash);
|
||||
|
||||
const rsvSignature = await signTransactionHash(signer, txHash);
|
||||
const packedSig = abiEncodePacked(
|
||||
{ type: "uint256", value: rsvSignature.r },
|
||||
{ type: "uint256", value: rsvSignature.s },
|
||||
{ type: "uint8", value: rsvSignature.v }
|
||||
);
|
||||
|
||||
console.log("Executing transaction");
|
||||
|
||||
return safe.execTransaction(
|
||||
to,
|
||||
value,
|
||||
data,
|
||||
operation,
|
||||
safeTxGas,
|
||||
baseGas,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
packedSig,
|
||||
overrides
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
signAndExecuteSafeTransaction,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user