@overcast-xyz/cli 0.1.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 +272 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +200 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.js +18 -0
- package/dist/commands/accept.d.ts +11 -0
- package/dist/commands/accept.js +112 -0
- package/dist/commands/assets.d.ts +9 -0
- package/dist/commands/assets.js +80 -0
- package/dist/commands/balance.d.ts +9 -0
- package/dist/commands/balance.js +74 -0
- package/dist/commands/deposit.d.ts +7 -0
- package/dist/commands/deposit.js +67 -0
- package/dist/commands/exercise.d.ts +14 -0
- package/dist/commands/exercise.js +88 -0
- package/dist/commands/key.d.ts +15 -0
- package/dist/commands/key.js +98 -0
- package/dist/commands/mm-webhook.d.ts +36 -0
- package/dist/commands/mm-webhook.js +152 -0
- package/dist/commands/mm.d.ts +37 -0
- package/dist/commands/mm.js +345 -0
- package/dist/commands/offer.d.ts +29 -0
- package/dist/commands/offer.js +149 -0
- package/dist/commands/option.d.ts +28 -0
- package/dist/commands/option.js +138 -0
- package/dist/commands/quote-webhook.d.ts +36 -0
- package/dist/commands/quote-webhook.js +152 -0
- package/dist/commands/quote.d.ts +37 -0
- package/dist/commands/quote.js +345 -0
- package/dist/commands/redeem.d.ts +14 -0
- package/dist/commands/redeem.js +88 -0
- package/dist/commands/rfq-view.d.ts +25 -0
- package/dist/commands/rfq-view.js +172 -0
- package/dist/commands/rfq.d.ts +4 -0
- package/dist/commands/rfq.js +280 -0
- package/dist/commands/shared.d.ts +81 -0
- package/dist/commands/shared.js +296 -0
- package/dist/commands/withdraw.d.ts +7 -0
- package/dist/commands/withdraw.js +67 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +58 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +50 -0
- package/package.json +38 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.runQuoter = runQuoter;
|
|
40
|
+
const p = __importStar(require("@clack/prompts"));
|
|
41
|
+
const core_1 = require("@overcast-xyz/core");
|
|
42
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
43
|
+
const client_1 = require("../client");
|
|
44
|
+
const config_1 = require("../config");
|
|
45
|
+
const quote_webhook_1 = require("./quote-webhook");
|
|
46
|
+
/** Quoter edge as a fraction of fair value (2%). */
|
|
47
|
+
const DEFAULT_SPREAD = 0.02;
|
|
48
|
+
/** Annualized implied volatility used for every quote (80%). */
|
|
49
|
+
const DEFAULT_VOL = 0.8;
|
|
50
|
+
/** Annualized risk-free rate (crypto convention: assume 0). */
|
|
51
|
+
const DEFAULT_RATE = 0;
|
|
52
|
+
/** Per-event webhook timeout when `--webhook-timeout` is omitted. */
|
|
53
|
+
const DEFAULT_WEBHOOK_TIMEOUT_MS = 5000;
|
|
54
|
+
const SECONDS_PER_YEAR = 365 * 24 * 60 * 60;
|
|
55
|
+
/**
|
|
56
|
+
* Symbols treated as the cash / numeraire leg. A quote needs exactly one cash
|
|
57
|
+
* leg and one volatile-underlying leg; the cash side is assumed to be ~$1.
|
|
58
|
+
*/
|
|
59
|
+
const STABLE_SYMBOLS = new Set([
|
|
60
|
+
"USDC",
|
|
61
|
+
"USDT",
|
|
62
|
+
"USDH",
|
|
63
|
+
"PYUSD",
|
|
64
|
+
"DAI",
|
|
65
|
+
"USDP",
|
|
66
|
+
"TUSD",
|
|
67
|
+
"USD",
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* `overcast quote` — run an RFQ quoter that auto-quotes every incoming RFQ
|
|
71
|
+
* using a Black-Scholes price.
|
|
72
|
+
*
|
|
73
|
+
* A cash-secured put and a covered call are the same instrument — an option to
|
|
74
|
+
* swap `settlementAmount` of one asset for `collateralAmount` of the other —
|
|
75
|
+
* differing only in which leg is collateral. We identify the stablecoin (cash)
|
|
76
|
+
* leg and the volatile (underlying) leg, then price:
|
|
77
|
+
* - settlement is cash → collateral is the underlying → **covered call**
|
|
78
|
+
* - collateral is cash → settlement is the underlying → **cash-secured put**
|
|
79
|
+
* See {@link blackScholesPricer}.
|
|
80
|
+
*/
|
|
81
|
+
async function runQuoter(opts) {
|
|
82
|
+
const config = (0, config_1.resolveConfig)(opts);
|
|
83
|
+
const app = await (0, client_1.createClient)(config);
|
|
84
|
+
const spread = opts.spread ? Number(opts.spread) : DEFAULT_SPREAD;
|
|
85
|
+
const vol = opts.vol ? Number(opts.vol) : DEFAULT_VOL;
|
|
86
|
+
const rate = opts.rate ? Number(opts.rate) : DEFAULT_RATE;
|
|
87
|
+
const rfq = app.rfq;
|
|
88
|
+
// Webhook mode: delegate pricing (and forward every lifecycle event) to a
|
|
89
|
+
// developer-supplied local endpoint. Falls back to the built-in Black-Scholes
|
|
90
|
+
// pricer when no `--webhook-url` (or `OVERCAST_QUOTE_WEBHOOK_URL`) is set.
|
|
91
|
+
const webhookUrl = opts.webhookUrl ?? process.env.OVERCAST_QUOTE_WEBHOOK_URL;
|
|
92
|
+
const webhook = webhookUrl
|
|
93
|
+
? {
|
|
94
|
+
url: webhookUrl,
|
|
95
|
+
timeoutMs: Number(opts.webhookTimeout) || DEFAULT_WEBHOOK_TIMEOUT_MS,
|
|
96
|
+
}
|
|
97
|
+
: null;
|
|
98
|
+
const pricer = webhook
|
|
99
|
+
? (0, quote_webhook_1.webhookPricer)(webhook, app.publicKey)
|
|
100
|
+
: blackScholesPricer({ spread, vol, rate });
|
|
101
|
+
p.intro(picocolors_1.default.magenta("Overcast RFQ Quoter"));
|
|
102
|
+
// The quoter takes the option (pays the premium), so it acts on the
|
|
103
|
+
// TAKER side — the opposite of the RFQ creator's MAKER side.
|
|
104
|
+
await rfq.registerRole("TAKER");
|
|
105
|
+
p.log.info(`Maker ${picocolors_1.default.bold(app.publicKey)}`);
|
|
106
|
+
p.log.info(picocolors_1.default.dim(webhook
|
|
107
|
+
? `Webhook — ${webhook.url} (timeout ${webhook.timeoutMs}ms)`
|
|
108
|
+
: `Black-Scholes — vol ${vol * 100}%, rate ${rate * 100}%, edge ${spread * 100}%`));
|
|
109
|
+
p.log.info(picocolors_1.default.dim("Listening for RFQs. Press Ctrl-C to stop."));
|
|
110
|
+
rfq.onQuoteAccepted = (a) => {
|
|
111
|
+
p.log.success(`Quote ${short(a.quoteId)} accepted — option ${picocolors_1.default.bold(a.optionId)} (RFQ ${short(a.rfqId)})`);
|
|
112
|
+
if (webhook)
|
|
113
|
+
(0, quote_webhook_1.postEvent)(webhook, "quoteAccepted", a, app.publicKey);
|
|
114
|
+
};
|
|
115
|
+
rfq.onError = (e) => {
|
|
116
|
+
p.log.error(`RFQ error: ${e.message}`);
|
|
117
|
+
if (webhook)
|
|
118
|
+
(0, quote_webhook_1.postEvent)(webhook, "error", e, app.publicKey);
|
|
119
|
+
};
|
|
120
|
+
// In webhook mode, forward the remaining lifecycle events too so the endpoint
|
|
121
|
+
// has full visibility (these are fire-and-forget — their replies are ignored).
|
|
122
|
+
if (webhook) {
|
|
123
|
+
rfq.onNewQuote = (q) => (0, quote_webhook_1.postEvent)(webhook, "newQuote", q, app.publicKey);
|
|
124
|
+
rfq.onRfqCancelled = (c) => (0, quote_webhook_1.postEvent)(webhook, "rfqCancelled", c, app.publicKey);
|
|
125
|
+
rfq.onRfqExpired = (e) => (0, quote_webhook_1.postEvent)(webhook, "rfqExpired", e, app.publicKey);
|
|
126
|
+
}
|
|
127
|
+
// Auto-quote every incoming RFQ (Black-Scholes, or the webhook's reply).
|
|
128
|
+
rfq.onNewRfq = (incoming) => {
|
|
129
|
+
void quoteRfq(rfq, app.publicKey, incoming, pricer);
|
|
130
|
+
};
|
|
131
|
+
// Run until interrupted.
|
|
132
|
+
await new Promise((resolve) => {
|
|
133
|
+
process.on("SIGINT", () => {
|
|
134
|
+
p.log.warn("Shutting down…");
|
|
135
|
+
rfq.disconnect();
|
|
136
|
+
resolve();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
p.outro(picocolors_1.default.magenta("Quoter stopped."));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Price an incoming RFQ and submit a quote over the full option terms — the
|
|
143
|
+
* RFQ's legs completed with the maker's premium — on the TAKER side. The client
|
|
144
|
+
* authors the settlement offer and signs it with the protocol signer, so no
|
|
145
|
+
* signature is passed here.
|
|
146
|
+
*/
|
|
147
|
+
async function quoteRfq(rfq, creator, incoming, pricer) {
|
|
148
|
+
try {
|
|
149
|
+
const premium = await pricer(incoming);
|
|
150
|
+
if (!premium)
|
|
151
|
+
return; // the pricer already logged why it skipped
|
|
152
|
+
const details = (0, core_1.completeOptionDetails)(incoming.details, premium);
|
|
153
|
+
if (!details) {
|
|
154
|
+
skip(incoming.rfqId, "RFQ is missing option terms");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const { quoteId } = await rfq.submitQuote({
|
|
158
|
+
rfqId: incoming.rfqId,
|
|
159
|
+
details,
|
|
160
|
+
creator,
|
|
161
|
+
side: "TAKER",
|
|
162
|
+
});
|
|
163
|
+
p.log.success(`Quoted ${premium.premiumAmount} ${short(premium.premiumAsset)} on RFQ ${short(incoming.rfqId)} (quote ${short(quoteId)})`);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
p.log.error(`Failed to quote RFQ ${short(incoming.rfqId)}: ${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Black-Scholes pricer. Maps an RFQ onto European-option terms and quotes the
|
|
171
|
+
* fair premium plus a `spread` edge, denominated in the cash (stablecoin) leg.
|
|
172
|
+
*
|
|
173
|
+
* The contract lets the holder swap the underlying for cash at the implied
|
|
174
|
+
* strike `K = cashAmount / underlyingAmount` (USD per underlying unit), with
|
|
175
|
+
* `underlyingAmount` as the contract size.
|
|
176
|
+
*
|
|
177
|
+
* Spot comes from a live CoinGecko lookup, falling back to the asset's
|
|
178
|
+
* backend-supplied {@link Asset.lastKnownPriceUsd} (the only source for RWAs,
|
|
179
|
+
* which have no `coinGeckoId`). Volatility uses the asset's own
|
|
180
|
+
* {@link Asset.volatility} when present, else the configured default. Returns
|
|
181
|
+
* `null` (skips the RFQ) when the legs can't be priced — no single cash leg, no
|
|
182
|
+
* usable spot price, or the option has already expired.
|
|
183
|
+
*/
|
|
184
|
+
function blackScholesPricer(params) {
|
|
185
|
+
const { spread, vol, rate } = params;
|
|
186
|
+
return async (rfq) => {
|
|
187
|
+
const Sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
188
|
+
await Sleep(Math.random() * 1000);
|
|
189
|
+
// `d` is the curated (wire) shape — assets resolved, amounts as decimal
|
|
190
|
+
// strings — but only *partially* filled: the RFQ may omit some terms. We can
|
|
191
|
+
// only price a fully-specified pair of legs, so bail otherwise.
|
|
192
|
+
const d = rfq.details;
|
|
193
|
+
if (d.endTimestamp == null ||
|
|
194
|
+
!d.collateralAsset ||
|
|
195
|
+
d.collateralAmount == null ||
|
|
196
|
+
!d.settlementAsset ||
|
|
197
|
+
d.settlementAmount == null) {
|
|
198
|
+
skip(rfq.rfqId, "RFQ does not specify both option legs");
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
// Capture the now-narrowed terms before any further `await` widens them back.
|
|
202
|
+
const endTimestamp = d.endTimestamp;
|
|
203
|
+
const collateral = {
|
|
204
|
+
asset: d.collateralAsset,
|
|
205
|
+
amount: (0, core_1.toDisplayAmount)((0, core_1.amountFromWire)(d.collateralAmount), d.collateralAsset),
|
|
206
|
+
};
|
|
207
|
+
const settlement = {
|
|
208
|
+
asset: d.settlementAsset,
|
|
209
|
+
amount: (0, core_1.toDisplayAmount)((0, core_1.amountFromWire)(d.settlementAmount), d.settlementAsset),
|
|
210
|
+
};
|
|
211
|
+
// Need exactly one cash leg and one volatile leg to define an option.
|
|
212
|
+
const collateralIsCash = isStable(collateral.asset);
|
|
213
|
+
const settlementIsCash = isStable(settlement.asset);
|
|
214
|
+
if (collateralIsCash === settlementIsCash) {
|
|
215
|
+
skip(rfq.rfqId, `need exactly one stablecoin leg (got ${symbol(collateral.asset)}/${symbol(settlement.asset)})`);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
// settlement = cash → covered call; collateral = cash → cash-secured put.
|
|
219
|
+
const isCall = settlementIsCash;
|
|
220
|
+
const cash = isCall ? settlement : collateral;
|
|
221
|
+
const underlying = isCall ? collateral : settlement;
|
|
222
|
+
// Prefer a live CoinGecko spot, falling back to the backend-supplied last
|
|
223
|
+
// known price. For RWAs (no `coinGeckoId`) the last known price is the only
|
|
224
|
+
// source — they're never auto-refreshed against a live feed.
|
|
225
|
+
const liveSpot = underlying.asset.coinGeckoId
|
|
226
|
+
? await fetchSpotUsd(underlying.asset.coinGeckoId)
|
|
227
|
+
: null;
|
|
228
|
+
const spot = liveSpot ?? underlying.asset.lastKnownPriceUsd ?? null;
|
|
229
|
+
if (spot == null) {
|
|
230
|
+
skip(rfq.rfqId, `no spot or last known price for ${symbol(underlying.asset)}`);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
// Use the asset's own volatility when the backend provides one; otherwise
|
|
234
|
+
// fall back to the configured default.
|
|
235
|
+
const sigma = underlying.asset.volatility ?? vol;
|
|
236
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
237
|
+
const years = (endTimestamp - nowSec) / SECONDS_PER_YEAR;
|
|
238
|
+
if (years <= 0) {
|
|
239
|
+
skip(rfq.rfqId, "option has already expired");
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const strike = Number(cash.amount) / Number(underlying.amount); // USD per underlying unit
|
|
243
|
+
const perUnit = blackScholes(isCall, spot, strike, years, rate, sigma);
|
|
244
|
+
const fairCash = perUnit * Number(underlying.amount); // total premium in cash units
|
|
245
|
+
const quotedCash = fairCash * (1 + spread) * (Math.random() * 0.05 + 0.95); // add the quoter's edge
|
|
246
|
+
// Pricing is done in floats (display units); the quoted premium becomes an
|
|
247
|
+
// exact native `bigint`. The backend requires a positive premium, so clamp
|
|
248
|
+
// to at least one native unit.
|
|
249
|
+
const decimals = cash.asset.decimals ?? 0;
|
|
250
|
+
const native = (0, core_1.parseUnits)(quotedCash.toFixed(decimals), decimals);
|
|
251
|
+
const premiumAmount = native < 1n ? 1n : native;
|
|
252
|
+
return { premiumAsset: cash.asset.address, premiumAmount };
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Black-Scholes price of one European option, in the same currency as `S`/`K`.
|
|
257
|
+
*
|
|
258
|
+
* @param isCall true for a call, false for a put
|
|
259
|
+
* @param S spot price of the underlying
|
|
260
|
+
* @param K strike price
|
|
261
|
+
* @param T time to expiry, in years
|
|
262
|
+
* @param r annualized risk-free rate
|
|
263
|
+
* @param sigma annualized volatility
|
|
264
|
+
*/
|
|
265
|
+
function blackScholes(isCall, S, K, T, r, sigma) {
|
|
266
|
+
// Degenerate inputs: the option is worth its (undiscounted) intrinsic value.
|
|
267
|
+
if (sigma <= 0 || T <= 0) {
|
|
268
|
+
return Math.max(0, isCall ? S - K : K - S);
|
|
269
|
+
}
|
|
270
|
+
const sqrtT = Math.sqrt(T);
|
|
271
|
+
const d1 = (Math.log(S / K) + (r + (sigma * sigma) / 2) * T) / (sigma * sqrtT);
|
|
272
|
+
const d2 = d1 - sigma * sqrtT;
|
|
273
|
+
const disc = Math.exp(-r * T);
|
|
274
|
+
return isCall
|
|
275
|
+
? S * normCdf(d1) - K * disc * normCdf(d2)
|
|
276
|
+
: K * disc * normCdf(-d2) - S * normCdf(-d1);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Standard normal CDF via the Zelen & Severo rational approximation
|
|
280
|
+
* (Abramowitz & Stegun 26.2.17), accurate to ~7.5e-8.
|
|
281
|
+
*/
|
|
282
|
+
function normCdf(x) {
|
|
283
|
+
if (x < 0)
|
|
284
|
+
return 1 - normCdf(-x);
|
|
285
|
+
const t = 1 / (1 + 0.2316419 * x);
|
|
286
|
+
const poly = t *
|
|
287
|
+
(0.31938153 +
|
|
288
|
+
t *
|
|
289
|
+
(-0.356563782 +
|
|
290
|
+
t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
|
|
291
|
+
const pdf = 0.3989422804014327 * Math.exp((-x * x) / 2);
|
|
292
|
+
return 1 - pdf * poly;
|
|
293
|
+
}
|
|
294
|
+
// --- spot prices -------------------------------------------------------------
|
|
295
|
+
/** Cached CoinGecko USD prices, refreshed on a short TTL to avoid rate limits. */
|
|
296
|
+
const SPOT_TTL_MS = 60_000;
|
|
297
|
+
const spotCache = new Map();
|
|
298
|
+
/**
|
|
299
|
+
* Fetch the USD spot price for a CoinGecko id, served from a short-lived cache.
|
|
300
|
+
* Returns `null` if the lookup fails (network error or unknown id) so the
|
|
301
|
+
* pricer can skip the RFQ rather than quote on stale or missing data.
|
|
302
|
+
*/
|
|
303
|
+
async function fetchSpotUsd(coinGeckoId) {
|
|
304
|
+
const cached = spotCache.get(coinGeckoId);
|
|
305
|
+
if (cached && Date.now() - cached.at < SPOT_TTL_MS)
|
|
306
|
+
return cached.price;
|
|
307
|
+
try {
|
|
308
|
+
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(coinGeckoId)}&vs_currencies=usd`;
|
|
309
|
+
const res = await fetch(url);
|
|
310
|
+
if (!res.ok)
|
|
311
|
+
return null;
|
|
312
|
+
const body = (await res.json());
|
|
313
|
+
const price = body[coinGeckoId]?.usd;
|
|
314
|
+
if (typeof price !== "number" || !(price > 0))
|
|
315
|
+
return null;
|
|
316
|
+
spotCache.set(coinGeckoId, { price, at: Date.now() });
|
|
317
|
+
return price;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// --- helpers -----------------------------------------------------------------
|
|
324
|
+
/**
|
|
325
|
+
* Whether an asset is treated as the cash / numeraire leg. The backend's own
|
|
326
|
+
* {@link Asset.cashLike} classification wins when present; otherwise fall back to
|
|
327
|
+
* the stablecoin-symbol heuristic for assets that predate the flag.
|
|
328
|
+
*/
|
|
329
|
+
function isStable(asset) {
|
|
330
|
+
if (asset.cashLike != null)
|
|
331
|
+
return asset.cashLike;
|
|
332
|
+
return asset.symbol ? STABLE_SYMBOLS.has(asset.symbol.toUpperCase()) : false;
|
|
333
|
+
}
|
|
334
|
+
/** A display label for an asset (symbol, falling back to a short address). */
|
|
335
|
+
function symbol(asset) {
|
|
336
|
+
return asset.symbol ?? short(asset.address);
|
|
337
|
+
}
|
|
338
|
+
/** Log why an RFQ was skipped without quoting. */
|
|
339
|
+
function skip(rfqId, reason) {
|
|
340
|
+
p.log.warn(`Skipping RFQ ${short(rfqId)}: ${reason}`);
|
|
341
|
+
}
|
|
342
|
+
/** Shorten a base58 id for log lines. */
|
|
343
|
+
function short(id) {
|
|
344
|
+
return id.length <= 12 ? id : `${id.slice(0, 6)}…${id.slice(-4)}`;
|
|
345
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GlobalFlags } from "../config";
|
|
2
|
+
export interface RedeemOptions extends GlobalFlags {
|
|
3
|
+
collateralOffer?: string;
|
|
4
|
+
settlementOffer?: string;
|
|
5
|
+
/** Native-unit amount of collateral-return claims to burn. */
|
|
6
|
+
amount?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* `overcast redeem` — redeem (burn collateral-return claims of) an option,
|
|
10
|
+
* pulling the maker's collateral (and any settlement paid on exercise) back
|
|
11
|
+
* out. Payouts always go to the maker; the wallet must be the maker to hold
|
|
12
|
+
* claims to redeem.
|
|
13
|
+
*/
|
|
14
|
+
export declare function redeem(opts: RedeemOptions): Promise<void>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.redeem = redeem;
|
|
40
|
+
const p = __importStar(require("@clack/prompts"));
|
|
41
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
42
|
+
const client_1 = require("../client");
|
|
43
|
+
const config_1 = require("../config");
|
|
44
|
+
const shared_1 = require("./shared");
|
|
45
|
+
/**
|
|
46
|
+
* `overcast redeem` — redeem (burn collateral-return claims of) an option,
|
|
47
|
+
* pulling the maker's collateral (and any settlement paid on exercise) back
|
|
48
|
+
* out. Payouts always go to the maker; the wallet must be the maker to hold
|
|
49
|
+
* claims to redeem.
|
|
50
|
+
*/
|
|
51
|
+
async function redeem(opts) {
|
|
52
|
+
const config = (0, config_1.resolveConfig)(opts);
|
|
53
|
+
const app = await (0, client_1.createClient)(config);
|
|
54
|
+
const registry = await (0, shared_1.loadRegistry)(app.api);
|
|
55
|
+
p.intro(picocolors_1.default.cyan("Overcast redeem"));
|
|
56
|
+
const option = await (0, shared_1.resolveOption)(app.chain, opts);
|
|
57
|
+
if (app.publicKey !== option.maker) {
|
|
58
|
+
p.log.warn(`This wallet is not the option maker (${(0, shared_1.short)(option.maker)}); the redeem will fail.`);
|
|
59
|
+
}
|
|
60
|
+
const claims = await app.chain.redeemClaims(option, app.publicKey);
|
|
61
|
+
p.note([
|
|
62
|
+
(0, shared_1.describeOptionDetails)(option.details, registry),
|
|
63
|
+
"",
|
|
64
|
+
`Your redeem claims ${picocolors_1.default.bold(claims.toString())}`,
|
|
65
|
+
].join("\n"), `Option ${(0, shared_1.short)(option.id)}`);
|
|
66
|
+
if (claims === 0n && opts.amount == null) {
|
|
67
|
+
p.outro(picocolors_1.default.yellow("You hold no collateral-return claims for this option."));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const amount = opts.amount != null
|
|
71
|
+
? BigInt(opts.amount)
|
|
72
|
+
: await (0, shared_1.promptAmountUpTo)("Amount to redeem", claims);
|
|
73
|
+
if (!(await (0, shared_1.confirm)(`Redeem ${amount} claim(s)?`))) {
|
|
74
|
+
p.outro(picocolors_1.default.yellow("Cancelled."));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const spinner = p.spinner();
|
|
78
|
+
spinner.start(`Redeeming ${amount} claim(s)`);
|
|
79
|
+
try {
|
|
80
|
+
const { hash } = await app.chain.redeemOption(option, amount);
|
|
81
|
+
spinner.stop(picocolors_1.default.green(`Redeemed. tx ${picocolors_1.default.bold(hash)}`));
|
|
82
|
+
p.outro(picocolors_1.default.green("Done."));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
spinner.stop(picocolors_1.default.red(`Redeem failed: ${err.message}`));
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GlobalFlags } from "../config";
|
|
2
|
+
export interface RfqListOptions extends GlobalFlags {
|
|
3
|
+
/** Filter to RFQs created by this address. */
|
|
4
|
+
creator?: string;
|
|
5
|
+
/** Filter by side: MAKER or TAKER. */
|
|
6
|
+
side?: string;
|
|
7
|
+
/** Filter by lifecycle status (open, quoted, accepted, cancelled, expired). */
|
|
8
|
+
status?: string;
|
|
9
|
+
/** Max rows to return. */
|
|
10
|
+
limit?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `overcast rfq list` — browse persisted RFQs (open and historical) via the
|
|
14
|
+
* off-chain {@link OvercastView}. Read-only: no wallet required.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rfqList(opts: RfqListOptions): Promise<void>;
|
|
17
|
+
export interface RfqShowOptions extends GlobalFlags {
|
|
18
|
+
/** RFQ id to show; prompts when omitted. */
|
|
19
|
+
id?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `overcast rfq show` — show a single RFQ's terms and every quote submitted
|
|
23
|
+
* against it, via the off-chain {@link OvercastView}. Read-only.
|
|
24
|
+
*/
|
|
25
|
+
export declare function rfqShow(opts: RfqShowOptions): Promise<void>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.rfqList = rfqList;
|
|
40
|
+
exports.rfqShow = rfqShow;
|
|
41
|
+
const p = __importStar(require("@clack/prompts"));
|
|
42
|
+
const core_1 = require("@overcast/core");
|
|
43
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
44
|
+
const client_1 = require("../client");
|
|
45
|
+
const config_1 = require("../config");
|
|
46
|
+
const shared_1 = require("./shared");
|
|
47
|
+
const SIDES = ["MAKER", "TAKER"];
|
|
48
|
+
const STATUSES = [
|
|
49
|
+
"open",
|
|
50
|
+
"quoted",
|
|
51
|
+
"accepted",
|
|
52
|
+
"cancelled",
|
|
53
|
+
"expired",
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* `overcast rfq list` — browse persisted RFQs (open and historical) via the
|
|
57
|
+
* off-chain {@link OvercastView}. Read-only: no wallet required.
|
|
58
|
+
*/
|
|
59
|
+
async function rfqList(opts) {
|
|
60
|
+
const config = (0, config_1.resolveConfig)(opts, { keypairOptional: true });
|
|
61
|
+
const app = await (0, client_1.createClient)(config);
|
|
62
|
+
const filter = {
|
|
63
|
+
creator: (0, shared_1.optionalAddress)("--creator", opts.creator),
|
|
64
|
+
side: parseSide(opts.side),
|
|
65
|
+
status: parseStatus(opts.status),
|
|
66
|
+
};
|
|
67
|
+
const limit = (0, shared_1.parseLimit)(opts.limit);
|
|
68
|
+
p.intro(picocolors_1.default.cyan("Overcast RFQs"));
|
|
69
|
+
const spinner = p.spinner();
|
|
70
|
+
spinner.start("Loading RFQs");
|
|
71
|
+
try {
|
|
72
|
+
const page = await app.view.listRfqs({
|
|
73
|
+
filter,
|
|
74
|
+
page: limit ? { limit } : undefined,
|
|
75
|
+
});
|
|
76
|
+
spinner.stop(`${page.items.length} RFQ(s)`);
|
|
77
|
+
if (page.items.length === 0) {
|
|
78
|
+
p.outro(picocolors_1.default.yellow("No RFQs match."));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const rfq of page.items) {
|
|
82
|
+
p.log.message([
|
|
83
|
+
`${picocolors_1.default.bold((0, shared_1.short)(rfq.rfqId))} ${statusTag(rfq.status)} ${picocolors_1.default.dim(rfq.side)} ${(0, shared_1.summarizeCuratedDetails)(rfq.details)}`,
|
|
84
|
+
picocolors_1.default.dim(`creator ${(0, shared_1.short)(rfq.creator)} · ${(0, shared_1.formatTimestamp)(Math.floor(rfq.createdAt / 1000))}`),
|
|
85
|
+
].join("\n"));
|
|
86
|
+
}
|
|
87
|
+
p.outro(picocolors_1.default.green(`${page.total ?? page.items.length} total.`));
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
spinner.stop(picocolors_1.default.red(`List failed: ${err.message}`));
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `overcast rfq show` — show a single RFQ's terms and every quote submitted
|
|
96
|
+
* against it, via the off-chain {@link OvercastView}. Read-only.
|
|
97
|
+
*/
|
|
98
|
+
async function rfqShow(opts) {
|
|
99
|
+
const config = (0, config_1.resolveConfig)(opts, { keypairOptional: true });
|
|
100
|
+
const app = await (0, client_1.createClient)(config);
|
|
101
|
+
if (opts.id && (0, shared_1.validateAddress)(opts.id)) {
|
|
102
|
+
throw new Error(`invalid RFQ id: ${opts.id}`);
|
|
103
|
+
}
|
|
104
|
+
const id = opts.id ?? (await (0, shared_1.promptId)("RFQ id"));
|
|
105
|
+
p.intro(picocolors_1.default.cyan("Overcast RFQ"));
|
|
106
|
+
const spinner = p.spinner();
|
|
107
|
+
spinner.start("Loading RFQ");
|
|
108
|
+
try {
|
|
109
|
+
const rfq = await app.view.getRfq(id);
|
|
110
|
+
if (!rfq) {
|
|
111
|
+
spinner.stop(picocolors_1.default.yellow(`No RFQ found for ${(0, shared_1.short)(id)}.`));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const quotes = await app.view.listQuotes(id);
|
|
115
|
+
spinner.stop(`${quotes.items.length} quote(s)`);
|
|
116
|
+
p.note([
|
|
117
|
+
`Creator ${(0, shared_1.short)(rfq.creator)}`,
|
|
118
|
+
`Side ${rfq.side}`,
|
|
119
|
+
`Status ${statusTag(rfq.status)}`,
|
|
120
|
+
"",
|
|
121
|
+
(0, shared_1.summarizeCuratedDetails)(rfq.details),
|
|
122
|
+
].join("\n"), `RFQ ${(0, shared_1.short)(rfq.rfqId)}`);
|
|
123
|
+
if (quotes.items.length === 0) {
|
|
124
|
+
p.log.info(picocolors_1.default.dim("No quotes yet."));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
for (const quote of quotes.items) {
|
|
128
|
+
p.log.message([
|
|
129
|
+
`${picocolors_1.default.bold((0, shared_1.short)(quote.quoteId))} ${(0, core_1.formatAssetAmount)((0, core_1.amountFromWire)(quote.details.premiumAmount), quote.details.premiumAsset)} ${picocolors_1.default.dim(quote.status)}`,
|
|
130
|
+
picocolors_1.default.dim(`from ${(0, shared_1.short)(quote.creator)}`),
|
|
131
|
+
].join("\n"));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
p.outro(picocolors_1.default.green("Done."));
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
spinner.stop(picocolors_1.default.red(`Lookup failed: ${err.message}`));
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Colour an RFQ status for scannable list output. */
|
|
142
|
+
function statusTag(status) {
|
|
143
|
+
switch (status) {
|
|
144
|
+
case "open":
|
|
145
|
+
case "quoted":
|
|
146
|
+
return picocolors_1.default.green(status);
|
|
147
|
+
case "accepted":
|
|
148
|
+
return picocolors_1.default.cyan(status);
|
|
149
|
+
default:
|
|
150
|
+
return picocolors_1.default.dim(status);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Validate the optional `--side` filter against the {@link ClientRole} union. */
|
|
154
|
+
function parseSide(value) {
|
|
155
|
+
if (value == null)
|
|
156
|
+
return undefined;
|
|
157
|
+
const side = value.toUpperCase();
|
|
158
|
+
if (!SIDES.includes(side)) {
|
|
159
|
+
throw new Error(`invalid --side: ${value} (expected ${SIDES.join(" or ")})`);
|
|
160
|
+
}
|
|
161
|
+
return side;
|
|
162
|
+
}
|
|
163
|
+
/** Validate the optional `--status` filter against the {@link RfqStatus} union. */
|
|
164
|
+
function parseStatus(value) {
|
|
165
|
+
if (value == null)
|
|
166
|
+
return undefined;
|
|
167
|
+
const status = value.toLowerCase();
|
|
168
|
+
if (!STATUSES.includes(status)) {
|
|
169
|
+
throw new Error(`invalid --status: ${value} (expected ${STATUSES.join(", ")})`);
|
|
170
|
+
}
|
|
171
|
+
return status;
|
|
172
|
+
}
|