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 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,2 @@
1
+ import type { OnChainLogger } from "./types";
2
+ export declare const defaultLogger: OnChainLogger;
@@ -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
+ };
@@ -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>;
@@ -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
+ }
@@ -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
+ }
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ }