@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.
Files changed (44) hide show
  1. package/README.md +272 -0
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +200 -0
  4. package/dist/client.d.ts +12 -0
  5. package/dist/client.js +18 -0
  6. package/dist/commands/accept.d.ts +11 -0
  7. package/dist/commands/accept.js +112 -0
  8. package/dist/commands/assets.d.ts +9 -0
  9. package/dist/commands/assets.js +80 -0
  10. package/dist/commands/balance.d.ts +9 -0
  11. package/dist/commands/balance.js +74 -0
  12. package/dist/commands/deposit.d.ts +7 -0
  13. package/dist/commands/deposit.js +67 -0
  14. package/dist/commands/exercise.d.ts +14 -0
  15. package/dist/commands/exercise.js +88 -0
  16. package/dist/commands/key.d.ts +15 -0
  17. package/dist/commands/key.js +98 -0
  18. package/dist/commands/mm-webhook.d.ts +36 -0
  19. package/dist/commands/mm-webhook.js +152 -0
  20. package/dist/commands/mm.d.ts +37 -0
  21. package/dist/commands/mm.js +345 -0
  22. package/dist/commands/offer.d.ts +29 -0
  23. package/dist/commands/offer.js +149 -0
  24. package/dist/commands/option.d.ts +28 -0
  25. package/dist/commands/option.js +138 -0
  26. package/dist/commands/quote-webhook.d.ts +36 -0
  27. package/dist/commands/quote-webhook.js +152 -0
  28. package/dist/commands/quote.d.ts +37 -0
  29. package/dist/commands/quote.js +345 -0
  30. package/dist/commands/redeem.d.ts +14 -0
  31. package/dist/commands/redeem.js +88 -0
  32. package/dist/commands/rfq-view.d.ts +25 -0
  33. package/dist/commands/rfq-view.js +172 -0
  34. package/dist/commands/rfq.d.ts +4 -0
  35. package/dist/commands/rfq.js +280 -0
  36. package/dist/commands/shared.d.ts +81 -0
  37. package/dist/commands/shared.js +296 -0
  38. package/dist/commands/withdraw.d.ts +7 -0
  39. package/dist/commands/withdraw.js +67 -0
  40. package/dist/config.d.ts +53 -0
  41. package/dist/config.js +58 -0
  42. package/dist/index.d.ts +18 -0
  43. package/dist/index.js +50 -0
  44. 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>;