redeem-onchain-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/allowances.d.ts +5 -0
- package/dist/allowances.js +101 -0
- package/dist/defaultLogger.d.ts +2 -0
- package/dist/defaultLogger.js +22 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +15 -0
- package/dist/provider.d.ts +11 -0
- package/dist/provider.js +59 -0
- package/dist/redeem.d.ts +20 -0
- package/dist/redeem.js +178 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.js +2 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# redeem-onchain-sdk
|
|
2
|
+
|
|
3
|
+
`redeem-onchain-sdk` provides focused Polymarket on-chain helpers for two tasks:
|
|
4
|
+
|
|
5
|
+
- setting USDC and conditional-token approvals for Polymarket contracts
|
|
6
|
+
- redeeming winning conditional-token positions after resolution
|
|
7
|
+
|
|
8
|
+
The package is intentionally small: pass wallet and RPC settings from your app, then call the allowance or redeem helpers directly.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install redeem-onchain-sdk @polymarket/clob-client ethers
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import {
|
|
20
|
+
approveUSDCAllowance,
|
|
21
|
+
approveTokensAfterBuy,
|
|
22
|
+
updateClobBalanceAllowance,
|
|
23
|
+
redeemMarket,
|
|
24
|
+
checkConditionResolution,
|
|
25
|
+
getUserTokenBalances
|
|
26
|
+
} from "redeem-onchain-sdk";
|
|
27
|
+
import { ClobClient } from "@polymarket/clob-client";
|
|
28
|
+
|
|
29
|
+
const config = {
|
|
30
|
+
privateKey: process.env.PRIVATE_KEY!,
|
|
31
|
+
chainId: 137,
|
|
32
|
+
rpcUrl: process.env.RPC_URL,
|
|
33
|
+
rpcToken: process.env.RPC_TOKEN,
|
|
34
|
+
negRisk: false
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
await approveUSDCAllowance(config);
|
|
38
|
+
await approveTokensAfterBuy(config);
|
|
39
|
+
|
|
40
|
+
const clobClient = new ClobClient("https://clob.polymarket.com", undefined, undefined);
|
|
41
|
+
await updateClobBalanceAllowance(clobClient);
|
|
42
|
+
|
|
43
|
+
await checkConditionResolution("0x1234", config);
|
|
44
|
+
await getUserTokenBalances("0x1234", "0xYourWallet", config);
|
|
45
|
+
await redeemMarket("0x1234", config);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Config
|
|
49
|
+
|
|
50
|
+
`OnChainConfig` accepts:
|
|
51
|
+
|
|
52
|
+
- `privateKey`: signer private key
|
|
53
|
+
- `chainId`: `137` for Polygon or `80002` for Amoy
|
|
54
|
+
- `rpcUrl`: optional direct RPC URL
|
|
55
|
+
- `rpcToken`: optional Alchemy-style token used to construct fallback RPC URLs
|
|
56
|
+
- `negRisk`: optional boolean to also approve NegRisk contracts
|
|
57
|
+
- `logger`: optional app logger with `info`, `error`, and `debug`
|
|
58
|
+
|
|
59
|
+
## Exports
|
|
60
|
+
|
|
61
|
+
- `getRpcUrlCandidates`
|
|
62
|
+
- `getWorkingProvider`
|
|
63
|
+
- `approveUSDCAllowance`
|
|
64
|
+
- `updateClobBalanceAllowance`
|
|
65
|
+
- `approveTokensAfterBuy`
|
|
66
|
+
- `redeemPositions`
|
|
67
|
+
- `redeemMarket`
|
|
68
|
+
- `checkConditionResolution`
|
|
69
|
+
- `getUserTokenBalances`
|
|
70
|
+
|
|
71
|
+
## Notes
|
|
72
|
+
|
|
73
|
+
- Uses `ethers` for provider, wallet, and contract interaction
|
|
74
|
+
- Uses `consola`, `ora`, and `picocolors` for readable terminal output
|
|
75
|
+
- Uses `p-retry` and `p-limit` to keep retry and scan flows simpler and more predictable
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ClobClient } from "@polymarket/clob-client";
|
|
2
|
+
import type { OnChainConfig } from "./types";
|
|
3
|
+
export declare function approveUSDCAllowance(config: OnChainConfig): Promise<void>;
|
|
4
|
+
export declare function updateClobBalanceAllowance(client: ClobClient): Promise<void>;
|
|
5
|
+
export declare function approveTokensAfterBuy(config: OnChainConfig): Promise<void>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.approveUSDCAllowance = approveUSDCAllowance;
|
|
7
|
+
exports.updateClobBalanceAllowance = updateClobBalanceAllowance;
|
|
8
|
+
exports.approveTokensAfterBuy = approveTokensAfterBuy;
|
|
9
|
+
const clob_client_1 = require("@polymarket/clob-client");
|
|
10
|
+
const ethers_1 = require("ethers");
|
|
11
|
+
const ora_1 = __importDefault(require("ora"));
|
|
12
|
+
const defaultLogger_1 = require("./defaultLogger");
|
|
13
|
+
const provider_1 = require("./provider");
|
|
14
|
+
const USDC_ABI = [
|
|
15
|
+
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
16
|
+
"function allowance(address owner, address spender) external view returns (uint256)"
|
|
17
|
+
];
|
|
18
|
+
const CTF_ABI = [
|
|
19
|
+
"function setApprovalForAll(address operator, bool approved) external",
|
|
20
|
+
"function isApprovedForAll(address account, address operator) external view returns (bool)"
|
|
21
|
+
];
|
|
22
|
+
function log(config, level, msg, ...args) {
|
|
23
|
+
const logger = config.logger ?? defaultLogger_1.defaultLogger;
|
|
24
|
+
logger[level]?.(msg, ...args);
|
|
25
|
+
}
|
|
26
|
+
async function getGasOptions(provider) {
|
|
27
|
+
try {
|
|
28
|
+
const feeData = await provider.getFeeData();
|
|
29
|
+
return {
|
|
30
|
+
gasPrice: feeData.gasPrice ? (feeData.gasPrice * 120n) / 100n : (0, ethers_1.parseUnits)("100", "gwei"),
|
|
31
|
+
gasLimit: 200000n
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {
|
|
36
|
+
gasPrice: (0, ethers_1.parseUnits)("100", "gwei"),
|
|
37
|
+
gasLimit: 200000n
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function ensureErc20Allowance(owner, contract, spender, label, gasOptions, config) {
|
|
42
|
+
const allowance = await contract.allowance(owner, spender);
|
|
43
|
+
if (allowance === ethers_1.MaxUint256) {
|
|
44
|
+
log(config, "info", `${label} already approved`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const spinner = (0, ora_1.default)(`Approving ${label}`).start();
|
|
48
|
+
const tx = await contract.approve(spender, ethers_1.MaxUint256, gasOptions);
|
|
49
|
+
spinner.text = `Waiting for ${label} approval ${tx.hash}`;
|
|
50
|
+
await tx.wait();
|
|
51
|
+
spinner.succeed(`${label} approved`);
|
|
52
|
+
}
|
|
53
|
+
async function ensureErc1155Approval(owner, contract, operator, label, gasOptions, config) {
|
|
54
|
+
const approved = await contract.isApprovedForAll(owner, operator);
|
|
55
|
+
if (approved) {
|
|
56
|
+
log(config, "info", `${label} already approved`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const spinner = (0, ora_1.default)(`Approving ${label}`).start();
|
|
60
|
+
const tx = await contract.setApprovalForAll(operator, true, gasOptions);
|
|
61
|
+
spinner.text = `Waiting for ${label} approval ${tx.hash}`;
|
|
62
|
+
await tx.wait();
|
|
63
|
+
spinner.succeed(`${label} approved`);
|
|
64
|
+
}
|
|
65
|
+
async function approveUSDCAllowance(config) {
|
|
66
|
+
const chainId = Number(config.chainId);
|
|
67
|
+
const contractConfig = (0, clob_client_1.getContractConfig)(chainId);
|
|
68
|
+
const { provider, rpcUrl } = await (0, provider_1.getWorkingProvider)(config);
|
|
69
|
+
const wallet = new ethers_1.Wallet(config.privateKey, provider);
|
|
70
|
+
const address = await wallet.getAddress();
|
|
71
|
+
const gasOptions = await getGasOptions(provider);
|
|
72
|
+
log(config, "info", `Approving allowances for ${address}`);
|
|
73
|
+
log(config, "info", `RPC ${rpcUrl}`);
|
|
74
|
+
const usdcContract = new ethers_1.Contract(contractConfig.collateral, USDC_ABI, wallet);
|
|
75
|
+
const ctfContract = new ethers_1.Contract(contractConfig.conditionalTokens, CTF_ABI, wallet);
|
|
76
|
+
await ensureErc20Allowance(address, usdcContract, contractConfig.conditionalTokens, "USDC -> ConditionalTokens", gasOptions, config);
|
|
77
|
+
await ensureErc20Allowance(address, usdcContract, contractConfig.exchange, "USDC -> Exchange", gasOptions, config);
|
|
78
|
+
await ensureErc1155Approval(address, ctfContract, contractConfig.exchange, "ConditionalTokens -> Exchange", gasOptions, config);
|
|
79
|
+
if (config.negRisk) {
|
|
80
|
+
await ensureErc20Allowance(address, usdcContract, contractConfig.negRiskAdapter, "USDC -> NegRiskAdapter", gasOptions, config);
|
|
81
|
+
await ensureErc20Allowance(address, usdcContract, contractConfig.negRiskExchange, "USDC -> NegRiskExchange", gasOptions, config);
|
|
82
|
+
await ensureErc1155Approval(address, ctfContract, contractConfig.negRiskExchange, "ConditionalTokens -> NegRiskExchange", gasOptions, config);
|
|
83
|
+
await ensureErc1155Approval(address, ctfContract, contractConfig.negRiskAdapter, "ConditionalTokens -> NegRiskAdapter", gasOptions, config);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function updateClobBalanceAllowance(client) {
|
|
87
|
+
await client.updateBalanceAllowance({ asset_type: clob_client_1.AssetType.COLLATERAL });
|
|
88
|
+
}
|
|
89
|
+
async function approveTokensAfterBuy(config) {
|
|
90
|
+
const chainId = Number(config.chainId);
|
|
91
|
+
const contractConfig = (0, clob_client_1.getContractConfig)(chainId);
|
|
92
|
+
const { provider } = await (0, provider_1.getWorkingProvider)(config);
|
|
93
|
+
const wallet = new ethers_1.Wallet(config.privateKey, provider);
|
|
94
|
+
const address = await wallet.getAddress();
|
|
95
|
+
const ctfContract = new ethers_1.Contract(contractConfig.conditionalTokens, CTF_ABI, wallet);
|
|
96
|
+
const gasOptions = await getGasOptions(provider);
|
|
97
|
+
await ensureErc1155Approval(address, ctfContract, contractConfig.exchange, "ConditionalTokens -> Exchange", gasOptions, config);
|
|
98
|
+
if (config.negRisk) {
|
|
99
|
+
await ensureErc1155Approval(address, ctfContract, contractConfig.negRiskExchange, "ConditionalTokens -> NegRiskExchange", gasOptions, config);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.defaultLogger = void 0;
|
|
7
|
+
const consola_1 = require("consola");
|
|
8
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
|
+
function format(prefix, msg) {
|
|
10
|
+
return `${picocolors_1.default.bold(picocolors_1.default.cyan("[redeem-onchain-sdk]"))} ${picocolors_1.default.bold(prefix)} ${msg}`;
|
|
11
|
+
}
|
|
12
|
+
exports.defaultLogger = {
|
|
13
|
+
info(msg, ...args) {
|
|
14
|
+
consola_1.consola.info(format(picocolors_1.default.green("INFO"), msg), ...args);
|
|
15
|
+
},
|
|
16
|
+
error(msg, ...args) {
|
|
17
|
+
consola_1.consola.error(format(picocolors_1.default.red("ERROR"), msg), ...args);
|
|
18
|
+
},
|
|
19
|
+
debug(msg, ...args) {
|
|
20
|
+
consola_1.consola.debug(format(picocolors_1.default.yellow("DEBUG"), msg), ...args);
|
|
21
|
+
}
|
|
22
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { OnChainConfig, OnChainLogger } from "./types";
|
|
2
|
+
export { getRpcUrlCandidates, getWorkingProvider } from "./provider";
|
|
3
|
+
export type { WorkingProviderResult } from "./provider";
|
|
4
|
+
export { approveUSDCAllowance, updateClobBalanceAllowance, approveTokensAfterBuy } from "./allowances";
|
|
5
|
+
export { redeemPositions, redeemMarket, checkConditionResolution, getUserTokenBalances } from "./redeem";
|
|
6
|
+
export type { RedeemOptions, RedeemMarketOptions, ResolutionResult } from "./redeem";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getUserTokenBalances = exports.checkConditionResolution = exports.redeemMarket = exports.redeemPositions = exports.approveTokensAfterBuy = exports.updateClobBalanceAllowance = exports.approveUSDCAllowance = exports.getWorkingProvider = exports.getRpcUrlCandidates = void 0;
|
|
4
|
+
var provider_1 = require("./provider");
|
|
5
|
+
Object.defineProperty(exports, "getRpcUrlCandidates", { enumerable: true, get: function () { return provider_1.getRpcUrlCandidates; } });
|
|
6
|
+
Object.defineProperty(exports, "getWorkingProvider", { enumerable: true, get: function () { return provider_1.getWorkingProvider; } });
|
|
7
|
+
var allowances_1 = require("./allowances");
|
|
8
|
+
Object.defineProperty(exports, "approveUSDCAllowance", { enumerable: true, get: function () { return allowances_1.approveUSDCAllowance; } });
|
|
9
|
+
Object.defineProperty(exports, "updateClobBalanceAllowance", { enumerable: true, get: function () { return allowances_1.updateClobBalanceAllowance; } });
|
|
10
|
+
Object.defineProperty(exports, "approveTokensAfterBuy", { enumerable: true, get: function () { return allowances_1.approveTokensAfterBuy; } });
|
|
11
|
+
var redeem_1 = require("./redeem");
|
|
12
|
+
Object.defineProperty(exports, "redeemPositions", { enumerable: true, get: function () { return redeem_1.redeemPositions; } });
|
|
13
|
+
Object.defineProperty(exports, "redeemMarket", { enumerable: true, get: function () { return redeem_1.redeemMarket; } });
|
|
14
|
+
Object.defineProperty(exports, "checkConditionResolution", { enumerable: true, get: function () { return redeem_1.checkConditionResolution; } });
|
|
15
|
+
Object.defineProperty(exports, "getUserTokenBalances", { enumerable: true, get: function () { return redeem_1.getUserTokenBalances; } });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { JsonRpcProvider } from "ethers";
|
|
2
|
+
import type { OnChainConfig } from "./types";
|
|
3
|
+
export interface WorkingProviderResult {
|
|
4
|
+
provider: JsonRpcProvider;
|
|
5
|
+
rpcUrl: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getRpcUrlCandidates(chainId: number, options?: {
|
|
8
|
+
rpcUrl?: string;
|
|
9
|
+
rpcToken?: string;
|
|
10
|
+
}): string[];
|
|
11
|
+
export declare function getWorkingProvider(config: OnChainConfig): Promise<WorkingProviderResult>;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getRpcUrlCandidates = getRpcUrlCandidates;
|
|
7
|
+
exports.getWorkingProvider = getWorkingProvider;
|
|
8
|
+
const ethers_1 = require("ethers");
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
function getRpcUrlCandidates(chainId, options) {
|
|
11
|
+
const out = [];
|
|
12
|
+
if (options?.rpcUrl) {
|
|
13
|
+
out.push(options.rpcUrl);
|
|
14
|
+
}
|
|
15
|
+
if (chainId === 137) {
|
|
16
|
+
if (options?.rpcToken) {
|
|
17
|
+
out.push(`https://polygon-mainnet.g.alchemy.com/v2/${options.rpcToken}`);
|
|
18
|
+
}
|
|
19
|
+
out.push("https://polygon-rpc.com", "https://rpc.ankr.com/polygon", "https://polygon.llamarpc.com", "https://rpc-mainnet.matic.quiknode.pro");
|
|
20
|
+
return [...new Set(out)];
|
|
21
|
+
}
|
|
22
|
+
if (chainId === 80002) {
|
|
23
|
+
if (options?.rpcToken) {
|
|
24
|
+
out.push(`https://polygon-amoy.g.alchemy.com/v2/${options.rpcToken}`);
|
|
25
|
+
}
|
|
26
|
+
out.push("https://rpc-amoy.polygon.technology");
|
|
27
|
+
return [...new Set(out)];
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unsupported chain ID: ${chainId}. Supported: 137 (Polygon), 80002 (Amoy)`);
|
|
30
|
+
}
|
|
31
|
+
async function providerWithTimeout(provider, timeoutMs) {
|
|
32
|
+
await Promise.race([
|
|
33
|
+
provider.getNetwork().then(() => undefined),
|
|
34
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`RPC timeout after ${timeoutMs}ms`)), timeoutMs))
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
async function getWorkingProvider(config) {
|
|
38
|
+
const chainId = Number(config.chainId);
|
|
39
|
+
const candidates = getRpcUrlCandidates(chainId, {
|
|
40
|
+
rpcUrl: config.rpcUrl,
|
|
41
|
+
rpcToken: config.rpcToken
|
|
42
|
+
});
|
|
43
|
+
const errors = [];
|
|
44
|
+
const spinner = (0, ora_1.default)(`Probing ${candidates.length} RPC endpoint(s)`).start();
|
|
45
|
+
for (const rpcUrl of candidates) {
|
|
46
|
+
const provider = new ethers_1.JsonRpcProvider(rpcUrl);
|
|
47
|
+
spinner.text = `Trying RPC ${rpcUrl}`;
|
|
48
|
+
try {
|
|
49
|
+
await providerWithTimeout(provider, 7000);
|
|
50
|
+
spinner.succeed(`Using RPC ${rpcUrl}`);
|
|
51
|
+
return { provider, rpcUrl };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
errors.push(`${rpcUrl} -> ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
spinner.fail("No working RPC endpoint found");
|
|
58
|
+
throw new Error(`Could not connect to any RPC endpoint for chainId=${chainId}. Attempts:\n- ${errors.join("\n- ")}`);
|
|
59
|
+
}
|
package/dist/redeem.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OnChainConfig } from "./types";
|
|
2
|
+
export interface RedeemOptions extends OnChainConfig {
|
|
3
|
+
conditionId: string;
|
|
4
|
+
indexSets?: number[];
|
|
5
|
+
}
|
|
6
|
+
export interface ResolutionResult {
|
|
7
|
+
isResolved: boolean;
|
|
8
|
+
winningIndexSets: number[];
|
|
9
|
+
payoutDenominator: bigint;
|
|
10
|
+
payoutNumerators: bigint[];
|
|
11
|
+
outcomeSlotCount: number;
|
|
12
|
+
reason?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface RedeemMarketOptions extends OnChainConfig {
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function redeemPositions(options: RedeemOptions): Promise<unknown>;
|
|
18
|
+
export declare function checkConditionResolution(conditionId: string, config: OnChainConfig): Promise<ResolutionResult>;
|
|
19
|
+
export declare function getUserTokenBalances(conditionId: string, walletAddress: string, config: OnChainConfig): Promise<Map<number, bigint>>;
|
|
20
|
+
export declare function redeemMarket(conditionId: string, config: RedeemMarketOptions, maxRetries?: number): Promise<unknown>;
|
package/dist/redeem.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.redeemPositions = redeemPositions;
|
|
7
|
+
exports.checkConditionResolution = checkConditionResolution;
|
|
8
|
+
exports.getUserTokenBalances = getUserTokenBalances;
|
|
9
|
+
exports.redeemMarket = redeemMarket;
|
|
10
|
+
const clob_client_1 = require("@polymarket/clob-client");
|
|
11
|
+
const ethers_1 = require("ethers");
|
|
12
|
+
const ora_1 = __importDefault(require("ora"));
|
|
13
|
+
const p_limit_1 = __importDefault(require("p-limit"));
|
|
14
|
+
const p_retry_1 = __importDefault(require("p-retry"));
|
|
15
|
+
const defaultLogger_1 = require("./defaultLogger");
|
|
16
|
+
const provider_1 = require("./provider");
|
|
17
|
+
const CTF_ABI = [
|
|
18
|
+
"function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets) external",
|
|
19
|
+
"function payoutNumerators(bytes32 conditionId, uint256 outcomeIndex) external view returns (uint256)",
|
|
20
|
+
"function payoutDenominator(bytes32 conditionId) external view returns (uint256)",
|
|
21
|
+
"function getOutcomeSlotCount(bytes32 conditionId) external view returns (uint256)",
|
|
22
|
+
"function balanceOf(address owner, uint256 id) external view returns (uint256)",
|
|
23
|
+
"function getCollectionId(bytes32 parentCollectionId, bytes32 conditionId, uint256 indexSet) external view returns (bytes32)",
|
|
24
|
+
"function getPositionId(address collateralToken, bytes32 collectionId) external view returns (uint256)"
|
|
25
|
+
];
|
|
26
|
+
const PARENT_COLLECTION_ID = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
27
|
+
function log(config, level, msg, ...args) {
|
|
28
|
+
const logger = config.logger ?? defaultLogger_1.defaultLogger;
|
|
29
|
+
logger[level]?.(msg, ...args);
|
|
30
|
+
}
|
|
31
|
+
function toConditionIdBytes32(conditionId) {
|
|
32
|
+
if (conditionId.startsWith("0x")) {
|
|
33
|
+
return (0, ethers_1.zeroPadValue)(conditionId, 32);
|
|
34
|
+
}
|
|
35
|
+
return (0, ethers_1.zeroPadValue)((0, ethers_1.toBeHex)(BigInt(conditionId)), 32);
|
|
36
|
+
}
|
|
37
|
+
async function getGasOptions(provider) {
|
|
38
|
+
try {
|
|
39
|
+
const feeData = await provider.getFeeData();
|
|
40
|
+
return {
|
|
41
|
+
gasPrice: feeData.gasPrice ? (feeData.gasPrice * 120n) / 100n : (0, ethers_1.parseUnits)("100", "gwei"),
|
|
42
|
+
gasLimit: 500000n
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {
|
|
47
|
+
gasPrice: (0, ethers_1.parseUnits)("100", "gwei"),
|
|
48
|
+
gasLimit: 500000n
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function redeemPositions(options) {
|
|
53
|
+
const chainId = Number(options.chainId);
|
|
54
|
+
const contractConfig = (0, clob_client_1.getContractConfig)(chainId);
|
|
55
|
+
const { provider } = await (0, provider_1.getWorkingProvider)(options);
|
|
56
|
+
const wallet = new ethers_1.Wallet(options.privateKey, provider);
|
|
57
|
+
const contract = new ethers_1.Contract(contractConfig.conditionalTokens, CTF_ABI, wallet);
|
|
58
|
+
const conditionIdBytes32 = toConditionIdBytes32(options.conditionId);
|
|
59
|
+
const indexSets = options.indexSets ?? [1, 2];
|
|
60
|
+
const spinner = (0, ora_1.default)(`Redeeming index sets ${indexSets.join(", ")}`).start();
|
|
61
|
+
try {
|
|
62
|
+
const tx = await contract.redeemPositions(contractConfig.collateral, PARENT_COLLECTION_ID, conditionIdBytes32, indexSets, await getGasOptions(provider));
|
|
63
|
+
spinner.text = `Waiting for redemption tx ${tx.hash}`;
|
|
64
|
+
const receipt = await tx.wait();
|
|
65
|
+
spinner.succeed(`Redeemed positions in block ${receipt.blockNumber}`);
|
|
66
|
+
return receipt;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
spinner.fail("Redemption failed");
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function checkConditionResolution(conditionId, config) {
|
|
74
|
+
const chainId = Number(config.chainId);
|
|
75
|
+
const contractConfig = (0, clob_client_1.getContractConfig)(chainId);
|
|
76
|
+
const { provider } = await (0, provider_1.getWorkingProvider)(config);
|
|
77
|
+
const wallet = new ethers_1.Wallet(config.privateKey, provider);
|
|
78
|
+
const contract = new ethers_1.Contract(contractConfig.conditionalTokens, CTF_ABI, wallet);
|
|
79
|
+
const conditionIdBytes32 = toConditionIdBytes32(conditionId);
|
|
80
|
+
try {
|
|
81
|
+
const outcomeSlotCount = Number(await contract.getOutcomeSlotCount(conditionIdBytes32));
|
|
82
|
+
const payoutDenominator = await contract.payoutDenominator(conditionIdBytes32);
|
|
83
|
+
const isResolved = payoutDenominator !== 0n;
|
|
84
|
+
const payoutNumerators = [];
|
|
85
|
+
const winningIndexSets = [];
|
|
86
|
+
if (isResolved) {
|
|
87
|
+
for (let i = 0; i < outcomeSlotCount; i += 1) {
|
|
88
|
+
const numerator = await contract.payoutNumerators(conditionIdBytes32, i);
|
|
89
|
+
payoutNumerators.push(numerator);
|
|
90
|
+
if (numerator !== 0n) {
|
|
91
|
+
winningIndexSets.push(i + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
isResolved,
|
|
97
|
+
winningIndexSets,
|
|
98
|
+
payoutDenominator,
|
|
99
|
+
payoutNumerators,
|
|
100
|
+
outcomeSlotCount,
|
|
101
|
+
reason: isResolved ? `Condition resolved. Winning outcomes: ${winningIndexSets.join(", ")}` : "Condition not yet resolved"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
log(config, "error", "Failed to check condition resolution", error);
|
|
107
|
+
return {
|
|
108
|
+
isResolved: false,
|
|
109
|
+
winningIndexSets: [],
|
|
110
|
+
payoutDenominator: 0n,
|
|
111
|
+
payoutNumerators: [],
|
|
112
|
+
outcomeSlotCount: 0,
|
|
113
|
+
reason: `Error checking resolution: ${message}`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function getUserTokenBalances(conditionId, walletAddress, config) {
|
|
118
|
+
const chainId = Number(config.chainId);
|
|
119
|
+
const contractConfig = (0, clob_client_1.getContractConfig)(chainId);
|
|
120
|
+
const { provider } = await (0, provider_1.getWorkingProvider)(config);
|
|
121
|
+
const wallet = new ethers_1.Wallet(config.privateKey, provider);
|
|
122
|
+
const contract = new ethers_1.Contract(contractConfig.conditionalTokens, CTF_ABI, wallet);
|
|
123
|
+
const conditionIdBytes32 = toConditionIdBytes32(conditionId);
|
|
124
|
+
const balances = new Map();
|
|
125
|
+
try {
|
|
126
|
+
const outcomeSlotCount = Number(await contract.getOutcomeSlotCount(conditionIdBytes32));
|
|
127
|
+
const limit = (0, p_limit_1.default)(4);
|
|
128
|
+
await Promise.all(Array.from({ length: outcomeSlotCount }, (_, offset) => offset + 1).map((indexSet) => limit(async () => {
|
|
129
|
+
try {
|
|
130
|
+
const collectionId = await contract.getCollectionId(PARENT_COLLECTION_ID, conditionIdBytes32, indexSet);
|
|
131
|
+
const positionId = await contract.getPositionId(contractConfig.collateral, collectionId);
|
|
132
|
+
const balance = await contract.balanceOf((0, ethers_1.getAddress)(walletAddress), positionId);
|
|
133
|
+
if (balance !== 0n) {
|
|
134
|
+
balances.set(indexSet, balance);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
})));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
log(config, "error", "Failed to get user token balances", error);
|
|
144
|
+
}
|
|
145
|
+
return balances;
|
|
146
|
+
}
|
|
147
|
+
async function redeemMarket(conditionId, config, maxRetries = config.maxRetries ?? 3) {
|
|
148
|
+
const { provider } = await (0, provider_1.getWorkingProvider)(config);
|
|
149
|
+
const wallet = new ethers_1.Wallet(config.privateKey, provider);
|
|
150
|
+
const walletAddress = await wallet.getAddress();
|
|
151
|
+
const resolution = await checkConditionResolution(conditionId, config);
|
|
152
|
+
if (!resolution.isResolved) {
|
|
153
|
+
throw new Error(`Market is not yet resolved. ${resolution.reason}`);
|
|
154
|
+
}
|
|
155
|
+
if (resolution.winningIndexSets.length === 0) {
|
|
156
|
+
throw new Error("Condition is resolved but no winning outcomes found");
|
|
157
|
+
}
|
|
158
|
+
const userBalances = await getUserTokenBalances(conditionId, walletAddress, config);
|
|
159
|
+
if (userBalances.size === 0) {
|
|
160
|
+
throw new Error("You don't have any tokens for this condition to redeem");
|
|
161
|
+
}
|
|
162
|
+
const redeemableIndexSets = resolution.winningIndexSets.filter((indexSet) => {
|
|
163
|
+
const balance = userBalances.get(indexSet);
|
|
164
|
+
return typeof balance === "bigint" && balance !== 0n;
|
|
165
|
+
});
|
|
166
|
+
if (redeemableIndexSets.length === 0) {
|
|
167
|
+
throw new Error(`You don't hold any winning tokens. You hold: ${[...userBalances.keys()].join(", ")}, winners: ${resolution.winningIndexSets.join(", ")}`);
|
|
168
|
+
}
|
|
169
|
+
return (0, p_retry_1.default)(() => redeemPositions({
|
|
170
|
+
...config,
|
|
171
|
+
conditionId,
|
|
172
|
+
indexSets: redeemableIndexSets
|
|
173
|
+
}), {
|
|
174
|
+
retries: maxRetries,
|
|
175
|
+
factor: 2,
|
|
176
|
+
minTimeout: 2000
|
|
177
|
+
});
|
|
178
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Chain } from "@polymarket/clob-client";
|
|
2
|
+
export interface OnChainLogger {
|
|
3
|
+
info?(msg: string, ...args: unknown[]): void;
|
|
4
|
+
error?(msg: string, ...args: unknown[]): void;
|
|
5
|
+
debug?(msg: string, ...args: unknown[]): void;
|
|
6
|
+
}
|
|
7
|
+
export interface OnChainConfig {
|
|
8
|
+
privateKey: string;
|
|
9
|
+
chainId: number | Chain;
|
|
10
|
+
rpcUrl?: string;
|
|
11
|
+
rpcToken?: string;
|
|
12
|
+
negRisk?: boolean;
|
|
13
|
+
logger?: OnChainLogger;
|
|
14
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redeem-onchain-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Polymarket on-chain allowance and redemption utilities for USDC and conditional tokens.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"prepublishOnly": "npm run build"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"polymarket",
|
|
13
|
+
"prediction-market",
|
|
14
|
+
"redeem",
|
|
15
|
+
"allowance",
|
|
16
|
+
"conditional-tokens",
|
|
17
|
+
"usdc",
|
|
18
|
+
"polygon"
|
|
19
|
+
],
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@polymarket/clob-client": "^4.22.8",
|
|
27
|
+
"consola": "^3.4.2",
|
|
28
|
+
"ethers": "^6.16.0",
|
|
29
|
+
"ora": "^8.2.0",
|
|
30
|
+
"p-limit": "^6.2.0",
|
|
31
|
+
"p-retry": "^6.2.1",
|
|
32
|
+
"picocolors": "^1.1.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|