@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,280 @@
|
|
|
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.rfqCreate = rfqCreate;
|
|
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 shared_1 = require("./shared");
|
|
46
|
+
const solana_program_1 = require("@overcast-xyz/solana-program");
|
|
47
|
+
/** `overcast rfq create` — request quotes, pick one, and settle it. */
|
|
48
|
+
async function rfqCreate(opts) {
|
|
49
|
+
const config = (0, config_1.resolveConfig)(opts);
|
|
50
|
+
const app = await (0, client_1.createClient)(config);
|
|
51
|
+
const rfq = app.rfq;
|
|
52
|
+
p.intro(picocolors_1.default.cyan("Overcast RFQ"));
|
|
53
|
+
try {
|
|
54
|
+
// The RFQ creator writes the option (posts the collateral), so it acts on
|
|
55
|
+
// the MAKER side; quoters respond on the opposite (TAKER) side.
|
|
56
|
+
await rfq.registerRole("MAKER");
|
|
57
|
+
const assets = await app.api.listAssets();
|
|
58
|
+
if (assets.length === 0)
|
|
59
|
+
throw new Error("backend has no supported assets");
|
|
60
|
+
const details = await promptRfqDetails(assets, app.settlement.defaultConfig());
|
|
61
|
+
// Wire up quote collection + error handling before creating.
|
|
62
|
+
const quotes = [];
|
|
63
|
+
let notify = null;
|
|
64
|
+
const wake = () => notify?.();
|
|
65
|
+
let terminal = null;
|
|
66
|
+
rfq.onNewQuote = (q) => {
|
|
67
|
+
quotes.push(q);
|
|
68
|
+
p.log.success(`Quote ${picocolors_1.default.dim(short(q.quoteId))}: ${(0, core_1.formatAssetAmount)((0, core_1.amountFromWire)(q.details.premiumAmount), q.details.premiumAsset)} from ${short(q.creator)}`);
|
|
69
|
+
wake();
|
|
70
|
+
};
|
|
71
|
+
rfq.onError = (e) => {
|
|
72
|
+
terminal = `error: ${e.message}`;
|
|
73
|
+
wake();
|
|
74
|
+
};
|
|
75
|
+
const { rfqId } = await rfq.createRfq({
|
|
76
|
+
creator: app.publicKey,
|
|
77
|
+
request: { details },
|
|
78
|
+
side: "MAKER",
|
|
79
|
+
});
|
|
80
|
+
p.log.info(`RFQ created: ${picocolors_1.default.bold(rfqId)} — waiting for quotes…`);
|
|
81
|
+
const chosen = await collectAndChoose(quotes, () => terminal, (fn) => {
|
|
82
|
+
notify = fn;
|
|
83
|
+
});
|
|
84
|
+
if (!chosen) {
|
|
85
|
+
p.outro(picocolors_1.default.yellow(terminal ?? "No quote selected."));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await acceptQuote(rfq, chosen);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
rfq.disconnect();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Accept a quote and settle it on-chain. The backend now settles inline and
|
|
96
|
+
* returns the freshly created option in the ack, so there is no separate
|
|
97
|
+
* `option:created` event to await.
|
|
98
|
+
*/
|
|
99
|
+
async function acceptQuote(rfq, quote) {
|
|
100
|
+
const spinner = p.spinner();
|
|
101
|
+
spinner.start("Accepting quote and settling on-chain");
|
|
102
|
+
try {
|
|
103
|
+
// The RFQ creator settles as the MAKER, so it authors a collateral offer
|
|
104
|
+
// over the quoted terms; the client signs it with the protocol signer. The
|
|
105
|
+
// quote's curated details are accepted as-is (uncurated internally).
|
|
106
|
+
const { optionId, txHash } = await rfq.acceptQuote({
|
|
107
|
+
quoteId: quote.quoteId,
|
|
108
|
+
side: "MAKER",
|
|
109
|
+
details: quote.details,
|
|
110
|
+
});
|
|
111
|
+
spinner.stop(picocolors_1.default.green(`Option created: ${picocolors_1.default.bold(optionId)} (accept tx ${short(txHash)})`));
|
|
112
|
+
p.outro(picocolors_1.default.green("Done."));
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
spinner.stop(picocolors_1.default.red(`Accept failed: ${err.message}`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Block until the user picks a quote or the RFQ terminates. New quotes appear
|
|
120
|
+
* as they arrive; the user can refresh to re-list before choosing.
|
|
121
|
+
*/
|
|
122
|
+
async function collectAndChoose(quotes, terminal, setNotify) {
|
|
123
|
+
for (;;) {
|
|
124
|
+
if (terminal())
|
|
125
|
+
return null;
|
|
126
|
+
if (quotes.length === 0) {
|
|
127
|
+
await new Promise((resolve) => setNotify(resolve));
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const selection = await p.select({
|
|
131
|
+
message: "Choose a quote to accept",
|
|
132
|
+
options: [
|
|
133
|
+
...quotes.map((q, i) => ({
|
|
134
|
+
value: String(i),
|
|
135
|
+
label: `${(0, core_1.formatAssetAmount)((0, core_1.amountFromWire)(q.details.premiumAmount), q.details.premiumAsset)} — ${short(q.creator)}`,
|
|
136
|
+
})),
|
|
137
|
+
{ value: "refresh", label: picocolors_1.default.dim("↻ wait for more quotes") },
|
|
138
|
+
{ value: "cancel", label: picocolors_1.default.dim("✗ cancel") },
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
const choice = (0, shared_1.ensureNotCancelled)(selection);
|
|
142
|
+
if (choice === "cancel")
|
|
143
|
+
return null;
|
|
144
|
+
if (choice === "refresh")
|
|
145
|
+
continue;
|
|
146
|
+
return quotes[Number(choice)];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Prompt for an option in the terms a trader actually thinks in — strategy,
|
|
151
|
+
* underlying, quantity, strike, expiry — and derive the raw collateral /
|
|
152
|
+
* settlement legs from them.
|
|
153
|
+
*
|
|
154
|
+
* A covered call and a cash-secured put are the same swap of `settlementAmount`
|
|
155
|
+
* of one asset for `collateralAmount` of the other, differing only in which leg
|
|
156
|
+
* is collateral (see the quote pricer in `quote.ts`):
|
|
157
|
+
* - **covered call** → collateral = underlying (qty), settlement = cash (strike × qty)
|
|
158
|
+
* - **cash-secured put** → collateral = cash (strike × qty), settlement = underlying (qty)
|
|
159
|
+
*
|
|
160
|
+
* where the strike is the cash price per unit of underlying. Premium is set by
|
|
161
|
+
* quoting MMs, so it isn't asked for here. Assets are picked from the backend's
|
|
162
|
+
* allowlist; the raw mint address is what's sent on the wire.
|
|
163
|
+
*/
|
|
164
|
+
async function promptRfqDetails(assets, settlementLayer) {
|
|
165
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
166
|
+
const byAddress = new Map(assets.map((a) => [a.address, a]));
|
|
167
|
+
const assetOption = (a) => ({
|
|
168
|
+
value: a.address,
|
|
169
|
+
label: `${a.symbol} ${picocolors_1.default.dim(short(a.address))}`,
|
|
170
|
+
});
|
|
171
|
+
const group = await p.group({
|
|
172
|
+
strategy: () => p.select({
|
|
173
|
+
message: "Strategy",
|
|
174
|
+
options: [
|
|
175
|
+
{
|
|
176
|
+
value: "call",
|
|
177
|
+
label: "Covered call",
|
|
178
|
+
hint: "post the underlying, get paid for upside",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
value: "put",
|
|
182
|
+
label: "Cash-secured put",
|
|
183
|
+
hint: "post cash, get paid to bid below spot",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
}),
|
|
187
|
+
underlyingAsset: () => p.select({
|
|
188
|
+
message: "Underlying asset",
|
|
189
|
+
options: assets.map(assetOption),
|
|
190
|
+
}),
|
|
191
|
+
quoteAsset: ({ results }) => p.select({
|
|
192
|
+
message: "Quote (cash) asset",
|
|
193
|
+
options: assets
|
|
194
|
+
.filter((a) => a.address !== results.underlyingAsset)
|
|
195
|
+
.map(assetOption),
|
|
196
|
+
}),
|
|
197
|
+
quantity: ({ results }) => {
|
|
198
|
+
const underlying = byAddress.get(results.underlyingAsset);
|
|
199
|
+
return p.text({
|
|
200
|
+
message: `Quantity (${underlying.symbol})`,
|
|
201
|
+
validate: (0, shared_1.validateDisplayAmount)(underlying),
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
strike: ({ results }) => {
|
|
205
|
+
const underlying = byAddress.get(results.underlyingAsset);
|
|
206
|
+
const quote = byAddress.get(results.quoteAsset);
|
|
207
|
+
const quantity = Number(results.quantity);
|
|
208
|
+
return p.text({
|
|
209
|
+
message: `Strike price (${quote.symbol} per ${underlying.symbol})`,
|
|
210
|
+
validate: (value) => {
|
|
211
|
+
const positive = (0, shared_1.validatePositiveNumber)(value);
|
|
212
|
+
if (positive)
|
|
213
|
+
return positive;
|
|
214
|
+
// The strike × quantity notional must round to at least one native
|
|
215
|
+
// unit of the cash leg, or there's nothing to settle.
|
|
216
|
+
try {
|
|
217
|
+
const notional = cashNotional(Number(value), quantity, quote);
|
|
218
|
+
if (notional < 1n) {
|
|
219
|
+
return `Notional rounds to zero ${quote.symbol}`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return "Enter a positive number";
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
expiryDays: () => p.text({
|
|
230
|
+
message: "Expiry (days from now)",
|
|
231
|
+
initialValue: "1",
|
|
232
|
+
validate: shared_1.validatePositiveNumber,
|
|
233
|
+
}),
|
|
234
|
+
}, { onCancel: () => process.exit(0) });
|
|
235
|
+
const isCall = group.strategy === "call";
|
|
236
|
+
const underlying = byAddress.get(group.underlyingAsset);
|
|
237
|
+
const quote = byAddress.get(group.quoteAsset);
|
|
238
|
+
const underlyingAmount = (0, core_1.toNativeAmount)(String(group.quantity), underlying);
|
|
239
|
+
const cashAmount = cashNotional(Number(group.strike), Number(group.quantity), quote);
|
|
240
|
+
const endTimestamp = nowSec + Math.round(Number(group.expiryDays) * 86_400);
|
|
241
|
+
// covered call → underlying is collateral; cash-secured put → cash is.
|
|
242
|
+
const collateralAsset = isCall ? underlying : quote;
|
|
243
|
+
const collateralAmount = isCall ? underlyingAmount : cashAmount;
|
|
244
|
+
const settlementAsset = isCall ? quote : underlying;
|
|
245
|
+
const settlementAmount = isCall ? cashAmount : underlyingAmount;
|
|
246
|
+
p.note([
|
|
247
|
+
`${picocolors_1.default.bold(isCall ? "Covered call" : "Cash-secured put")} on ${underlying.symbol}`,
|
|
248
|
+
`Quantity ${group.quantity} ${underlying.symbol}`,
|
|
249
|
+
`Strike ${group.strike} ${quote.symbol} / ${underlying.symbol}`,
|
|
250
|
+
`Expiry ${group.expiryDays}d`,
|
|
251
|
+
"",
|
|
252
|
+
`You post ${(0, core_1.formatAssetAmount)(collateralAmount, collateralAsset)}`,
|
|
253
|
+
`If settled ${(0, core_1.formatAssetAmount)(settlementAmount, settlementAsset)}`,
|
|
254
|
+
].join("\n"), "Order summary");
|
|
255
|
+
return {
|
|
256
|
+
startTimestamp: nowSec,
|
|
257
|
+
endTimestamp,
|
|
258
|
+
collateralAsset: collateralAsset.address,
|
|
259
|
+
collateralAmount,
|
|
260
|
+
settlementAsset: settlementAsset.address,
|
|
261
|
+
settlementAmount,
|
|
262
|
+
domain: solana_program_1.SOLANA_DOMAIN, // TODO: the domain should be read by the on-chain contract
|
|
263
|
+
settlementLayer: settlementLayer,
|
|
264
|
+
borrowAllowed: false,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The cash leg's native amount for a `strike` (cash per underlying unit) and
|
|
269
|
+
* `quantity` of underlying: `strike × quantity`, rounded to the cash asset's
|
|
270
|
+
* decimals. Pricing here is display-level, so plain float math is fine — the
|
|
271
|
+
* result is converted to an exact native `bigint`.
|
|
272
|
+
*/
|
|
273
|
+
function cashNotional(strike, quantity, quote) {
|
|
274
|
+
const notional = (strike * quantity).toFixed(quote.decimals ?? 0);
|
|
275
|
+
return (0, core_1.toNativeAmount)(notional, quote);
|
|
276
|
+
}
|
|
277
|
+
/** Shorten a base58 id for log lines. */
|
|
278
|
+
function short(id) {
|
|
279
|
+
return id.length <= 12 ? id : `${id.slice(0, 6)}…${id.slice(-4)}`;
|
|
280
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Asset, AssetRegistry, CuratedOptionDetails, MarketOption, OptionDetails, OvercastReader, OvercastView, WithId } from "@overcast-xyz/core";
|
|
2
|
+
/** Abort cleanly when the user cancels a clack prompt (Ctrl-C). */
|
|
3
|
+
export declare function ensureNotCancelled<T>(value: T | symbol): T;
|
|
4
|
+
/**
|
|
5
|
+
* Clack `validate` adapter over the layer's address predicate
|
|
6
|
+
* (`OvercastUtils.isValidAddress`): returns an error string when invalid, or
|
|
7
|
+
* `undefined` when the address is well-formed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateAddress(value: string): string | undefined;
|
|
10
|
+
/** Validate a positive integer amount; returns an error string when invalid. */
|
|
11
|
+
export declare function validateAmount(value: string): string | undefined;
|
|
12
|
+
/** Validate a positive (finite) number; returns an error string when invalid. */
|
|
13
|
+
export declare function validatePositiveNumber(value: string): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Validate a positive display amount (whole tokens, fractions allowed) that
|
|
16
|
+
* scales to at least one native unit of `asset`. Returns an error string when
|
|
17
|
+
* invalid.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateDisplayAmount(asset: Asset): (value: string) => string | undefined;
|
|
20
|
+
/** Prompt for a mint / address with base58 validation. */
|
|
21
|
+
export declare function promptMint(message: string): Promise<string>;
|
|
22
|
+
/** Prompt for a positive integer (native-unit) amount. */
|
|
23
|
+
export declare function promptAmount(message: string): Promise<bigint>;
|
|
24
|
+
/** Prompt for a content-address / account id with base58 validation. */
|
|
25
|
+
export declare function promptId(message: string): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Prompt for a positive integer (native-unit) amount that cannot exceed `max` —
|
|
28
|
+
* used to cap a burn (exercise / redeem) at the wallet's claim balance.
|
|
29
|
+
*/
|
|
30
|
+
export declare function promptAmountUpTo(message: string, max: bigint): Promise<bigint>;
|
|
31
|
+
/** Prompt for a yes/no confirmation, aborting cleanly on cancel. */
|
|
32
|
+
export declare function confirm(message: string): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Fetch the backend's asset allowlist (via the app's {@link OvercastView}) and
|
|
35
|
+
* wrap it in an {@link AssetRegistry} for symbol / decimals resolution. Throws
|
|
36
|
+
* if the backend has no assets — every picker / formatter downstream needs at
|
|
37
|
+
* least one.
|
|
38
|
+
*/
|
|
39
|
+
export declare function loadRegistry(view: Pick<OvercastView, "listAssets">): Promise<AssetRegistry>;
|
|
40
|
+
/** Prompt the user to pick an allowlisted asset; returns its mint address. */
|
|
41
|
+
export declare function pickAsset(registry: AssetRegistry, message: string): Promise<Asset>;
|
|
42
|
+
/** Shorten a base58 id for compact display. */
|
|
43
|
+
export declare function short(id: string): string;
|
|
44
|
+
/** Format a unix-seconds timestamp as a compact UTC date-time. */
|
|
45
|
+
export declare function formatTimestamp(seconds: number): string;
|
|
46
|
+
/** The two content-address offer ids that together identify a market option. */
|
|
47
|
+
export interface OptionRef {
|
|
48
|
+
collateralOffer?: string;
|
|
49
|
+
settlementOffer?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a market option from its `(collateralOffer, settlementOffer)` id pair,
|
|
53
|
+
* prompting for whichever id wasn't passed as a flag. Throws when no matching
|
|
54
|
+
* option exists on-chain.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveOption(reader: Pick<OvercastReader, "getOption">, ref: OptionRef): Promise<WithId<MarketOption>>;
|
|
57
|
+
/**
|
|
58
|
+
* Render an option's terms as aligned, human-readable lines — each asset leg
|
|
59
|
+
* resolved against the registry so symbols and decimals show instead of raw
|
|
60
|
+
* mints. Shared by the option / offer / exercise / redeem / accept views.
|
|
61
|
+
*/
|
|
62
|
+
export declare function describeOptionDetails(details: OptionDetails, registry: AssetRegistry): string;
|
|
63
|
+
/** Render curated option details as aligned lines, for a detail (show) view. */
|
|
64
|
+
export declare function describeCuratedDetails(details: CuratedOptionDetails): string;
|
|
65
|
+
/**
|
|
66
|
+
* A compact one-line summary of (possibly partial) curated terms for list rows:
|
|
67
|
+
* `collateral → settlement · premium · exp <date>`. Legs missing from a partial
|
|
68
|
+
* (e.g. an RFQ pins everything but the premium) are simply omitted.
|
|
69
|
+
*/
|
|
70
|
+
export declare function summarizeCuratedDetails(details: Partial<CuratedOptionDetails>): string;
|
|
71
|
+
/**
|
|
72
|
+
* Parse a `--limit` flag to a positive integer page size, throwing on a
|
|
73
|
+
* malformed value. Returns `undefined` when the flag is absent, letting the
|
|
74
|
+
* backend apply its default page size.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseLimit(value?: string): number | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Validate an optional address-valued filter flag, throwing a flag-named error
|
|
79
|
+
* when present but malformed. Returns the trimmed value (or `undefined`).
|
|
80
|
+
*/
|
|
81
|
+
export declare function optionalAddress(flag: string, value?: string): string | undefined;
|
|
@@ -0,0 +1,296 @@
|
|
|
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.ensureNotCancelled = ensureNotCancelled;
|
|
40
|
+
exports.validateAddress = validateAddress;
|
|
41
|
+
exports.validateAmount = validateAmount;
|
|
42
|
+
exports.validatePositiveNumber = validatePositiveNumber;
|
|
43
|
+
exports.validateDisplayAmount = validateDisplayAmount;
|
|
44
|
+
exports.promptMint = promptMint;
|
|
45
|
+
exports.promptAmount = promptAmount;
|
|
46
|
+
exports.promptId = promptId;
|
|
47
|
+
exports.promptAmountUpTo = promptAmountUpTo;
|
|
48
|
+
exports.confirm = confirm;
|
|
49
|
+
exports.loadRegistry = loadRegistry;
|
|
50
|
+
exports.pickAsset = pickAsset;
|
|
51
|
+
exports.short = short;
|
|
52
|
+
exports.formatTimestamp = formatTimestamp;
|
|
53
|
+
exports.resolveOption = resolveOption;
|
|
54
|
+
exports.describeOptionDetails = describeOptionDetails;
|
|
55
|
+
exports.describeCuratedDetails = describeCuratedDetails;
|
|
56
|
+
exports.summarizeCuratedDetails = summarizeCuratedDetails;
|
|
57
|
+
exports.parseLimit = parseLimit;
|
|
58
|
+
exports.optionalAddress = optionalAddress;
|
|
59
|
+
const p = __importStar(require("@clack/prompts"));
|
|
60
|
+
const core_1 = require("@overcast-xyz/core");
|
|
61
|
+
const solana_client_1 = require("@overcast-xyz/solana-client");
|
|
62
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
63
|
+
/** Abort cleanly when the user cancels a clack prompt (Ctrl-C). */
|
|
64
|
+
function ensureNotCancelled(value) {
|
|
65
|
+
if (p.isCancel(value)) {
|
|
66
|
+
p.cancel("Cancelled.");
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Clack `validate` adapter over the layer's address predicate
|
|
73
|
+
* (`OvercastUtils.isValidAddress`): returns an error string when invalid, or
|
|
74
|
+
* `undefined` when the address is well-formed.
|
|
75
|
+
*/
|
|
76
|
+
function validateAddress(value) {
|
|
77
|
+
return solana_client_1.solanaUtils.isValidAddress(value)
|
|
78
|
+
? undefined
|
|
79
|
+
: "Not a valid base58 Solana address";
|
|
80
|
+
}
|
|
81
|
+
/** Validate a positive integer amount; returns an error string when invalid. */
|
|
82
|
+
function validateAmount(value) {
|
|
83
|
+
const n = Number(value);
|
|
84
|
+
if (!Number.isInteger(n) || n < 1)
|
|
85
|
+
return "Enter a positive integer amount";
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
/** Validate a positive (finite) number; returns an error string when invalid. */
|
|
89
|
+
function validatePositiveNumber(value) {
|
|
90
|
+
const n = Number(value);
|
|
91
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
92
|
+
return "Enter a positive number";
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate a positive display amount (whole tokens, fractions allowed) that
|
|
97
|
+
* scales to at least one native unit of `asset`. Returns an error string when
|
|
98
|
+
* invalid.
|
|
99
|
+
*/
|
|
100
|
+
function validateDisplayAmount(asset) {
|
|
101
|
+
return (value) => {
|
|
102
|
+
const n = Number(value);
|
|
103
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
104
|
+
return "Enter a positive amount";
|
|
105
|
+
let native;
|
|
106
|
+
try {
|
|
107
|
+
native = (0, core_1.toNativeAmount)(value.trim(), asset);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return "Enter a positive amount";
|
|
111
|
+
}
|
|
112
|
+
if (native < 1n) {
|
|
113
|
+
return `Amount is below the smallest unit of ${asset.symbol ?? "this asset"}`;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Prompt for a mint / address with base58 validation. */
|
|
119
|
+
async function promptMint(message) {
|
|
120
|
+
const value = await p.text({ message, validate: validateAddress });
|
|
121
|
+
return ensureNotCancelled(value).trim();
|
|
122
|
+
}
|
|
123
|
+
/** Prompt for a positive integer (native-unit) amount. */
|
|
124
|
+
async function promptAmount(message) {
|
|
125
|
+
const value = await p.text({ message, validate: validateAmount });
|
|
126
|
+
return BigInt(ensureNotCancelled(value).trim());
|
|
127
|
+
}
|
|
128
|
+
/** Prompt for a content-address / account id with base58 validation. */
|
|
129
|
+
async function promptId(message) {
|
|
130
|
+
const value = await p.text({ message, validate: validateAddress });
|
|
131
|
+
return ensureNotCancelled(value).trim();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Prompt for a positive integer (native-unit) amount that cannot exceed `max` —
|
|
135
|
+
* used to cap a burn (exercise / redeem) at the wallet's claim balance.
|
|
136
|
+
*/
|
|
137
|
+
async function promptAmountUpTo(message, max) {
|
|
138
|
+
const value = await p.text({
|
|
139
|
+
message: `${message} ${picocolors_1.default.dim(`(max ${max})`)}`,
|
|
140
|
+
validate: (v) => {
|
|
141
|
+
const base = validateAmount(v);
|
|
142
|
+
if (base)
|
|
143
|
+
return base;
|
|
144
|
+
if (BigInt(v.trim()) > max)
|
|
145
|
+
return `Exceeds your balance of ${max}`;
|
|
146
|
+
return undefined;
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
return BigInt(ensureNotCancelled(value).trim());
|
|
150
|
+
}
|
|
151
|
+
/** Prompt for a yes/no confirmation, aborting cleanly on cancel. */
|
|
152
|
+
async function confirm(message) {
|
|
153
|
+
return ensureNotCancelled(await p.confirm({ message }));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Fetch the backend's asset allowlist (via the app's {@link OvercastView}) and
|
|
157
|
+
* wrap it in an {@link AssetRegistry} for symbol / decimals resolution. Throws
|
|
158
|
+
* if the backend has no assets — every picker / formatter downstream needs at
|
|
159
|
+
* least one.
|
|
160
|
+
*/
|
|
161
|
+
async function loadRegistry(view) {
|
|
162
|
+
const assets = await view.listAssets();
|
|
163
|
+
if (assets.length === 0)
|
|
164
|
+
throw new Error("backend has no supported assets");
|
|
165
|
+
return new core_1.AssetRegistry(assets);
|
|
166
|
+
}
|
|
167
|
+
/** Prompt the user to pick an allowlisted asset; returns its mint address. */
|
|
168
|
+
async function pickAsset(registry, message) {
|
|
169
|
+
const assets = registry.list();
|
|
170
|
+
const choice = await p.select({
|
|
171
|
+
message,
|
|
172
|
+
options: assets.map((a) => ({
|
|
173
|
+
value: a.address,
|
|
174
|
+
label: `${a.symbol ?? "?"} ${picocolors_1.default.dim(short(a.address))}`,
|
|
175
|
+
})),
|
|
176
|
+
});
|
|
177
|
+
return registry.require(ensureNotCancelled(choice));
|
|
178
|
+
}
|
|
179
|
+
/** Shorten a base58 id for compact display. */
|
|
180
|
+
function short(id) {
|
|
181
|
+
return id.length <= 12 ? id : `${id.slice(0, 6)}…${id.slice(-4)}`;
|
|
182
|
+
}
|
|
183
|
+
/** A single asset leg formatted with its symbol, resolved against the registry. */
|
|
184
|
+
function leg(registry, asset, amount) {
|
|
185
|
+
return (0, core_1.formatAssetAmount)(amount, registry.getOrDefault(asset));
|
|
186
|
+
}
|
|
187
|
+
/** Format a unix-seconds timestamp as a compact UTC date-time. */
|
|
188
|
+
function formatTimestamp(seconds) {
|
|
189
|
+
return `${new Date(seconds * 1000)
|
|
190
|
+
.toISOString()
|
|
191
|
+
.replace("T", " ")
|
|
192
|
+
.slice(0, 16)} UTC`;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Resolve a market option from its `(collateralOffer, settlementOffer)` id pair,
|
|
196
|
+
* prompting for whichever id wasn't passed as a flag. Throws when no matching
|
|
197
|
+
* option exists on-chain.
|
|
198
|
+
*/
|
|
199
|
+
async function resolveOption(reader, ref) {
|
|
200
|
+
if (ref.collateralOffer && validateAddress(ref.collateralOffer)) {
|
|
201
|
+
throw new Error(`invalid --collateral-offer id: ${ref.collateralOffer}`);
|
|
202
|
+
}
|
|
203
|
+
if (ref.settlementOffer && validateAddress(ref.settlementOffer)) {
|
|
204
|
+
throw new Error(`invalid --settlement-offer id: ${ref.settlementOffer}`);
|
|
205
|
+
}
|
|
206
|
+
const collateralOffer = ref.collateralOffer ?? (await promptId("Collateral offer id"));
|
|
207
|
+
const settlementOffer = ref.settlementOffer ?? (await promptId("Settlement offer id"));
|
|
208
|
+
const option = await reader.getOption(collateralOffer, settlementOffer);
|
|
209
|
+
if (!option) {
|
|
210
|
+
throw new Error("no option found for that collateral / settlement offer id pair");
|
|
211
|
+
}
|
|
212
|
+
return option;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Render an option's terms as aligned, human-readable lines — each asset leg
|
|
216
|
+
* resolved against the registry so symbols and decimals show instead of raw
|
|
217
|
+
* mints. Shared by the option / offer / exercise / redeem / accept views.
|
|
218
|
+
*/
|
|
219
|
+
function describeOptionDetails(details, registry) {
|
|
220
|
+
return [
|
|
221
|
+
`Collateral ${leg(registry, details.collateralAsset, details.collateralAmount)}`,
|
|
222
|
+
`Settlement ${leg(registry, details.settlementAsset, details.settlementAmount)}`,
|
|
223
|
+
`Premium ${leg(registry, details.premiumAsset, details.premiumAmount)}`,
|
|
224
|
+
`Opens ${formatTimestamp(details.startTimestamp)}`,
|
|
225
|
+
`Expires ${formatTimestamp(details.endTimestamp)}`,
|
|
226
|
+
].join("\n");
|
|
227
|
+
}
|
|
228
|
+
/* -------------------------------------------------------------------------- */
|
|
229
|
+
/* Curated (view) rendering */
|
|
230
|
+
/* -------------------------------------------------------------------------- */
|
|
231
|
+
//
|
|
232
|
+
// The off-chain {@link OvercastView} returns *curated* shapes: each asset field
|
|
233
|
+
// is a resolved {@link Asset} and each native amount a decimal-string. So these
|
|
234
|
+
// need no registry — the metadata travels with the payload — unlike the raw
|
|
235
|
+
// on-chain shapes above.
|
|
236
|
+
/** A curated asset leg (`Asset` embedded, amount as a wire string) → "1.5 SOL". */
|
|
237
|
+
function curatedLeg(asset, amount) {
|
|
238
|
+
return (0, core_1.formatAssetAmount)((0, core_1.amountFromWire)(amount), asset);
|
|
239
|
+
}
|
|
240
|
+
/** Render curated option details as aligned lines, for a detail (show) view. */
|
|
241
|
+
function describeCuratedDetails(details) {
|
|
242
|
+
return [
|
|
243
|
+
`Collateral ${curatedLeg(details.collateralAsset, details.collateralAmount)}`,
|
|
244
|
+
`Settlement ${curatedLeg(details.settlementAsset, details.settlementAmount)}`,
|
|
245
|
+
`Premium ${curatedLeg(details.premiumAsset, details.premiumAmount)}`,
|
|
246
|
+
`Opens ${formatTimestamp(details.startTimestamp)}`,
|
|
247
|
+
`Expires ${formatTimestamp(details.endTimestamp)}`,
|
|
248
|
+
].join("\n");
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* A compact one-line summary of (possibly partial) curated terms for list rows:
|
|
252
|
+
* `collateral → settlement · premium · exp <date>`. Legs missing from a partial
|
|
253
|
+
* (e.g. an RFQ pins everything but the premium) are simply omitted.
|
|
254
|
+
*/
|
|
255
|
+
function summarizeCuratedDetails(details) {
|
|
256
|
+
const parts = [];
|
|
257
|
+
if (details.collateralAsset &&
|
|
258
|
+
details.collateralAmount != null &&
|
|
259
|
+
details.settlementAsset &&
|
|
260
|
+
details.settlementAmount != null) {
|
|
261
|
+
parts.push(`${curatedLeg(details.collateralAsset, details.collateralAmount)} ${picocolors_1.default.dim("→")} ${curatedLeg(details.settlementAsset, details.settlementAmount)}`);
|
|
262
|
+
}
|
|
263
|
+
if (details.premiumAsset && details.premiumAmount != null) {
|
|
264
|
+
parts.push(`premium ${curatedLeg(details.premiumAsset, details.premiumAmount)}`);
|
|
265
|
+
}
|
|
266
|
+
if (details.endTimestamp != null) {
|
|
267
|
+
parts.push(picocolors_1.default.dim(`exp ${formatTimestamp(details.endTimestamp)}`));
|
|
268
|
+
}
|
|
269
|
+
return parts.join(picocolors_1.default.dim(" · "));
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Parse a `--limit` flag to a positive integer page size, throwing on a
|
|
273
|
+
* malformed value. Returns `undefined` when the flag is absent, letting the
|
|
274
|
+
* backend apply its default page size.
|
|
275
|
+
*/
|
|
276
|
+
function parseLimit(value) {
|
|
277
|
+
if (value == null)
|
|
278
|
+
return undefined;
|
|
279
|
+
const n = Number(value);
|
|
280
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
281
|
+
throw new Error(`invalid --limit: ${value} (expected a positive integer)`);
|
|
282
|
+
}
|
|
283
|
+
return n;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Validate an optional address-valued filter flag, throwing a flag-named error
|
|
287
|
+
* when present but malformed. Returns the trimmed value (or `undefined`).
|
|
288
|
+
*/
|
|
289
|
+
function optionalAddress(flag, value) {
|
|
290
|
+
if (value == null)
|
|
291
|
+
return undefined;
|
|
292
|
+
const trimmed = value.trim();
|
|
293
|
+
if (validateAddress(trimmed))
|
|
294
|
+
throw new Error(`invalid ${flag}: ${value}`);
|
|
295
|
+
return trimmed;
|
|
296
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { GlobalFlags } from "../config";
|
|
2
|
+
export interface WithdrawOptions extends GlobalFlags {
|
|
3
|
+
mint?: string;
|
|
4
|
+
amount?: string;
|
|
5
|
+
}
|
|
6
|
+
/** `overcast withdraw` — pull funds back out of the wallet's escrow vault. */
|
|
7
|
+
export declare function withdraw(opts: WithdrawOptions): Promise<void>;
|