mishmar 0.0.0-stage → 0.2.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/LICENSE +174 -0
- package/README.md +77 -2
- package/dist/agentkey.d.ts +63 -0
- package/dist/agentkey.js +118 -0
- package/dist/canon.d.ts +14 -0
- package/dist/canon.js +64 -0
- package/dist/client.d.ts +119 -0
- package/dist/client.js +188 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/proposal.d.ts +108 -0
- package/dist/proposal.js +159 -0
- package/dist/signer.d.ts +139 -0
- package/dist/signer.js +392 -0
- package/dist/time.d.ts +5 -0
- package/dist/time.js +17 -0
- package/package.json +41 -4
package/dist/signer.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signer as a child process, or one already running.
|
|
3
|
+
*
|
|
4
|
+
* `Signer` starts `mishmar signer run --local …` with the agent's config and
|
|
5
|
+
* talks to it over a Unix socket (or, on Windows, localhost with a token).
|
|
6
|
+
* The child holds the agent's key and its halves of the venue keys; this
|
|
7
|
+
* process holds no secret at all. An order is an intent: the child turns it
|
|
8
|
+
* into a proposal signed with the agent's key, sends it one-shot where it can
|
|
9
|
+
* build the venue's message, through the channel otherwise, and hands back
|
|
10
|
+
* the decision as the engine recorded it. The agent's reads pass through the
|
|
11
|
+
* child the same way.
|
|
12
|
+
*
|
|
13
|
+
* const s = await new Signer().start(); // ~/.mishmar/agent.json, mishmar on PATH
|
|
14
|
+
* let d = await s.order({ tradingAccountId: "ta_…", symbol: "BTC", side: "buy", size: "0.001", price: "79500" });
|
|
15
|
+
* if (d.verdict === "require_approval") d = await s.wait(d.id);
|
|
16
|
+
* await s.stop();
|
|
17
|
+
*
|
|
18
|
+
* The child lives and dies with this process: it gets a pipe on stdin that
|
|
19
|
+
* is never written to and stops when the pipe closes, so it does not outlive
|
|
20
|
+
* a bot that crashed or was killed. A signer run on its own instead (systemd,
|
|
21
|
+
* launchd, Docker: `mishmar signer run --local unix:/path`) is reached with
|
|
22
|
+
* `Signer.connect`, which never starts or stops it; that is the way for
|
|
23
|
+
* several processes of one agent, or to keep the signer connected across bot
|
|
24
|
+
* restarts. One signer runs per config: a second one on the same config
|
|
25
|
+
* stops at once and says which process holds it.
|
|
26
|
+
*
|
|
27
|
+
* The binary is found through `MISHMAR_BIN`, the PATH, or
|
|
28
|
+
* `~/.mishmar/bin/<version>/mishmar`; `downloadBinary` fetches a release from
|
|
29
|
+
* GitHub and checks it against the release's checksums.
|
|
30
|
+
*/
|
|
31
|
+
import { execFile, spawn } from "node:child_process";
|
|
32
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
33
|
+
import { constants as fsConstants } from "node:fs";
|
|
34
|
+
import { access, chmod, mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
|
35
|
+
import * as http from "node:http";
|
|
36
|
+
import * as net from "node:net";
|
|
37
|
+
import { arch, homedir, platform, tmpdir } from "node:os";
|
|
38
|
+
import * as path from "node:path";
|
|
39
|
+
import { EngineError, resolved } from "./client.js";
|
|
40
|
+
export const RELEASES = "https://github.com/mishmar-io/mishmar/releases/download";
|
|
41
|
+
const ENV_BINARY = "MISHMAR_BIN";
|
|
42
|
+
const ENV_LOCAL_TOKEN = "MISHMAR_LOCAL_TOKEN";
|
|
43
|
+
/** The release asset's os_arch for this machine: linux_amd64, darwin_arm64, … */
|
|
44
|
+
export function platformName() {
|
|
45
|
+
const os = { linux: "linux", darwin: "darwin", win32: "windows" }[platform()];
|
|
46
|
+
const cpu = { x64: "amd64", arm64: "arm64" }[arch()];
|
|
47
|
+
if (!os || !cpu)
|
|
48
|
+
throw new Error(`no mishmar release for ${platform()}/${arch()}`);
|
|
49
|
+
return `${os}_${cpu}`;
|
|
50
|
+
}
|
|
51
|
+
function exe(name) {
|
|
52
|
+
return platform() === "win32" ? name + ".exe" : name;
|
|
53
|
+
}
|
|
54
|
+
async function executable(p) {
|
|
55
|
+
if (!p)
|
|
56
|
+
return false;
|
|
57
|
+
try {
|
|
58
|
+
await access(p, fsConstants.X_OK);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Where the mishmar binary is: the argument, `MISHMAR_BIN`, the PATH, or a
|
|
67
|
+
* release `downloadBinary` fetched. Throws with what to do when there is none.
|
|
68
|
+
*/
|
|
69
|
+
export async function findBinary(explicit) {
|
|
70
|
+
const candidates = [explicit, process.env[ENV_BINARY]];
|
|
71
|
+
const name = exe("mishmar");
|
|
72
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter))
|
|
73
|
+
if (dir)
|
|
74
|
+
candidates.push(path.join(dir, name));
|
|
75
|
+
const home = path.join(homedir(), ".mishmar", "bin");
|
|
76
|
+
try {
|
|
77
|
+
for (const version of (await readdir(home)).sort().reverse())
|
|
78
|
+
candidates.push(path.join(home, version, name));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// no downloads yet
|
|
82
|
+
}
|
|
83
|
+
for (const c of candidates)
|
|
84
|
+
if (await executable(c))
|
|
85
|
+
return c;
|
|
86
|
+
throw new Error("no mishmar binary: install it from https://github.com/mishmar-io/mishmar/releases (or call downloadBinary(version)), put it on the PATH or in MISHMAR_BIN");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Fetch the release `version` (a tag, v0.1.0) for this machine into `destDir`
|
|
90
|
+
* (default `~/.mishmar/bin/<version>`), after checking its SHA-256 against the
|
|
91
|
+
* release's SHA256SUMS. Returns the binary's path.
|
|
92
|
+
*/
|
|
93
|
+
export async function downloadBinary(version, destDir, fetchImpl = fetch) {
|
|
94
|
+
const name = `mishmar_${version}_${platformName()}`;
|
|
95
|
+
const base = `${RELEASES}/${version}`;
|
|
96
|
+
const sumsRes = await fetchImpl(`${base}/SHA256SUMS`);
|
|
97
|
+
if (!sumsRes.ok)
|
|
98
|
+
throw new Error(`${base}/SHA256SUMS: HTTP ${sumsRes.status}`);
|
|
99
|
+
let want;
|
|
100
|
+
for (const line of (await sumsRes.text()).split("\n")) {
|
|
101
|
+
const parts = line.trim().split(/\s+/);
|
|
102
|
+
if (parts.length === 2 && parts[1]?.replace(/^\*/, "") === `${name}.tar.gz`)
|
|
103
|
+
want = parts[0]?.toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
if (!want)
|
|
106
|
+
throw new Error(`${name}.tar.gz is not in the release's SHA256SUMS`);
|
|
107
|
+
const res = await fetchImpl(`${base}/${name}.tar.gz`);
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
throw new Error(`${base}/${name}.tar.gz: HTTP ${res.status}`);
|
|
110
|
+
const archive = Buffer.from(await res.arrayBuffer());
|
|
111
|
+
const got = createHash("sha256").update(archive).digest("hex");
|
|
112
|
+
if (got !== want)
|
|
113
|
+
throw new Error(`${name}.tar.gz: checksum ${got} is not the release's ${want}`);
|
|
114
|
+
const dir = destDir ?? path.join(homedir(), ".mishmar", "bin", version);
|
|
115
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
116
|
+
const work = await mkdtemp(path.join(tmpdir(), "mishmar-"));
|
|
117
|
+
try {
|
|
118
|
+
const file = path.join(work, `${name}.tar.gz`);
|
|
119
|
+
await writeFile(file, archive);
|
|
120
|
+
await new Promise((resolve, reject) => execFile("tar", ["-xzf", file, "-C", dir, exe("mishmar")], (err) => (err ? reject(err) : resolve())));
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await rm(work, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
const out = path.join(dir, exe("mishmar"));
|
|
126
|
+
await chmod(out, 0o755);
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The agent's signer, run as a child process and spoken to over its local API.
|
|
131
|
+
* The child stops when this process exits, however it exits. `Signer.connect`
|
|
132
|
+
* speaks to a signer that runs on its own instead.
|
|
133
|
+
*/
|
|
134
|
+
export class Signer {
|
|
135
|
+
options;
|
|
136
|
+
address;
|
|
137
|
+
token;
|
|
138
|
+
process;
|
|
139
|
+
#dir;
|
|
140
|
+
#stderrTail = [];
|
|
141
|
+
#attached = false;
|
|
142
|
+
#ready = false;
|
|
143
|
+
constructor(options = {}) {
|
|
144
|
+
this.options = options;
|
|
145
|
+
this.address = options.address;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Speak to a signer that already runs on its own (`mishmar signer run
|
|
149
|
+
* --local ADDRESS`, kept up by systemd, launchd or Docker), and wait until it
|
|
150
|
+
* answers. `address` is `unix:/path`, or a loopback host:port with the
|
|
151
|
+
* signer's token. Nothing is started, and `stop` leaves the signer running.
|
|
152
|
+
*/
|
|
153
|
+
static async connect(address, options = {}) {
|
|
154
|
+
const s = new Signer({ address, startTimeoutMs: options.startTimeoutMs, timeoutMs: options.timeoutMs });
|
|
155
|
+
s.#attached = true;
|
|
156
|
+
if (!address.startsWith("unix:")) {
|
|
157
|
+
s.token = options.token ?? process.env[ENV_LOCAL_TOKEN];
|
|
158
|
+
if (!s.token)
|
|
159
|
+
throw new Error(`${address}: a signer on a TCP port needs its token: pass { token } or set ${ENV_LOCAL_TOKEN}`);
|
|
160
|
+
}
|
|
161
|
+
return s.start();
|
|
162
|
+
}
|
|
163
|
+
/** Start the child and wait until its local API answers; for a signer from `connect`, only wait until it answers. */
|
|
164
|
+
async start() {
|
|
165
|
+
if (this.#attached) {
|
|
166
|
+
if (!this.#ready) {
|
|
167
|
+
await this.#waitReady(() => undefined);
|
|
168
|
+
this.#ready = true;
|
|
169
|
+
}
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
if (this.process)
|
|
173
|
+
return this;
|
|
174
|
+
const binary = await findBinary(this.options.binary);
|
|
175
|
+
const env = { ...process.env };
|
|
176
|
+
if (!this.address) {
|
|
177
|
+
if (platform() === "win32") {
|
|
178
|
+
this.address = `127.0.0.1:${await freePort()}`;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.#dir = await mkdtemp(path.join(tmpdir(), "mishmar-"));
|
|
182
|
+
this.address = "unix:" + path.join(this.#dir, "signer.sock");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!this.address.startsWith("unix:")) {
|
|
186
|
+
this.token ??= randomBytes(32).toString("base64url");
|
|
187
|
+
env[ENV_LOCAL_TOKEN] = this.token;
|
|
188
|
+
}
|
|
189
|
+
// The child stops when its stdin closes: when this process exits, however
|
|
190
|
+
// it exits. Nothing is ever written to it.
|
|
191
|
+
const args = ["signer", "run", "--local", this.address, "--log-level", this.options.logLevel ?? "info", "--exit-on-stdin-close"];
|
|
192
|
+
if (this.options.config)
|
|
193
|
+
args.push("--config", this.options.config);
|
|
194
|
+
const child = spawn(binary, args, { env, stdio: ["pipe", "ignore", "pipe"] });
|
|
195
|
+
child.stdin?.on("error", () => { }); // a child that exits first may close it under us
|
|
196
|
+
this.process = child;
|
|
197
|
+
let exited;
|
|
198
|
+
child.on("exit", (code, signal) => {
|
|
199
|
+
exited = { code, signal };
|
|
200
|
+
});
|
|
201
|
+
const onLog = this.options.onLog ?? ((line) => process.stderr.write(line + "\n"));
|
|
202
|
+
let pending = "";
|
|
203
|
+
child.stderr?.on("data", (chunk) => {
|
|
204
|
+
pending += chunk.toString("utf8");
|
|
205
|
+
const lines = pending.split("\n");
|
|
206
|
+
pending = lines.pop() ?? "";
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
this.#stderrTail.push(line);
|
|
209
|
+
if (this.#stderrTail.length > 40)
|
|
210
|
+
this.#stderrTail.shift();
|
|
211
|
+
onLog(line);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
await this.#waitReady(() => exited);
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
async #waitReady(exitedOf) {
|
|
218
|
+
const deadline = Date.now() + (this.options.startTimeoutMs ?? 15_000);
|
|
219
|
+
for (;;) {
|
|
220
|
+
const exited = exitedOf();
|
|
221
|
+
if (exited) {
|
|
222
|
+
const tail = this.#stderrTail.join("\n");
|
|
223
|
+
await this.#cleanup();
|
|
224
|
+
throw new Error(`the signer exited with status ${exited.code ?? exited.signal} before it was ready:\n${tail}`);
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
await this.#request("GET", "/health", { timeoutMs: 2_000 });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
if (err instanceof EngineError)
|
|
232
|
+
throw err;
|
|
233
|
+
if (Date.now() >= deadline) {
|
|
234
|
+
await this.stop();
|
|
235
|
+
throw new Error(`the signer did not answer on ${this.address} within ${this.options.startTimeoutMs ?? 15_000} ms`);
|
|
236
|
+
}
|
|
237
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Stop the child, gently then not: its stdin closes, which stops it, and it
|
|
243
|
+
* gets SIGTERM, then SIGKILL after 5 s. A signer from `connect` keeps
|
|
244
|
+
* running; this only forgets it was ready.
|
|
245
|
+
*/
|
|
246
|
+
async stop() {
|
|
247
|
+
if (this.#attached) {
|
|
248
|
+
this.#ready = false;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const child = this.process;
|
|
252
|
+
if (!child)
|
|
253
|
+
return;
|
|
254
|
+
try {
|
|
255
|
+
child.stdin?.destroy();
|
|
256
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
257
|
+
const gone = new Promise((resolve) => child.once("exit", () => resolve()));
|
|
258
|
+
child.kill("SIGTERM");
|
|
259
|
+
const timer = new Promise((resolve) => setTimeout(() => resolve("timeout"), 5_000));
|
|
260
|
+
if ((await Promise.race([gone, timer])) === "timeout") {
|
|
261
|
+
child.kill("SIGKILL");
|
|
262
|
+
await gone;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
await this.#cleanup();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async #cleanup() {
|
|
271
|
+
this.process = undefined;
|
|
272
|
+
if (this.#dir) {
|
|
273
|
+
await rm(this.#dir, { recursive: true, force: true });
|
|
274
|
+
this.#dir = undefined;
|
|
275
|
+
this.address = undefined;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Proposals
|
|
279
|
+
/** Send an intent (the local API's ProposeRequest) and get the decision back. */
|
|
280
|
+
async propose(intent) {
|
|
281
|
+
return (await this.#request("POST", "/propose", { body: intent }));
|
|
282
|
+
}
|
|
283
|
+
/** Propose an order. A limit order needs a price; a market order is priced at the engine's mark. */
|
|
284
|
+
async order(intent) {
|
|
285
|
+
return this.propose({ orderType: "limit", reduceOnly: false, validForSeconds: 60, channel: false, ...intent });
|
|
286
|
+
}
|
|
287
|
+
/** Cancel a resting order this agent placed, by the clientOrderId its decision carries. */
|
|
288
|
+
async cancel(tradingAccountId, symbol, clientOrderId, validForSeconds = 60) {
|
|
289
|
+
return this.propose({ tradingAccountId, symbol, kind: "cancel", targetClientOrderId: clientOrderId, validForSeconds });
|
|
290
|
+
}
|
|
291
|
+
// Reads, through the child
|
|
292
|
+
async health() {
|
|
293
|
+
return (await this.#request("GET", "/health"));
|
|
294
|
+
}
|
|
295
|
+
async decision(decisionId) {
|
|
296
|
+
return (await this.#request("GET", `/decisions/${encodeURIComponent(decisionId)}`));
|
|
297
|
+
}
|
|
298
|
+
async decisions(query = {}) {
|
|
299
|
+
return (await this.#request("GET", "/decisions", { query }));
|
|
300
|
+
}
|
|
301
|
+
async approvals(status) {
|
|
302
|
+
return (await this.#request("GET", "/approvals", { query: { status } }));
|
|
303
|
+
}
|
|
304
|
+
async positions() {
|
|
305
|
+
return (await this.#request("GET", "/positions"));
|
|
306
|
+
}
|
|
307
|
+
async pnl(query = {}) {
|
|
308
|
+
return (await this.#request("GET", "/pnl", { query }));
|
|
309
|
+
}
|
|
310
|
+
async tradingAccount(tradingAccountId) {
|
|
311
|
+
return (await this.#request("GET", `/trading-accounts/${encodeURIComponent(tradingAccountId)}`));
|
|
312
|
+
}
|
|
313
|
+
async venueRules(tradingAccountId, symbol) {
|
|
314
|
+
return (await this.#request("GET", `/trading-accounts/${encodeURIComponent(tradingAccountId)}/venue-rules`, { query: { symbol } }));
|
|
315
|
+
}
|
|
316
|
+
/** Poll a decision until it no longer waits for a person, or until timeoutMs. */
|
|
317
|
+
async wait(decisionId, { timeoutMs = 60_000, intervalMs = 1_000 } = {}) {
|
|
318
|
+
const deadline = Date.now() + timeoutMs;
|
|
319
|
+
for (;;) {
|
|
320
|
+
const d = await this.decision(decisionId);
|
|
321
|
+
if (resolved(d) || Date.now() >= deadline)
|
|
322
|
+
return d;
|
|
323
|
+
await new Promise((r) => setTimeout(r, Math.min(intervalMs, Math.max(0, deadline - Date.now()))));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Plumbing
|
|
327
|
+
#request(method, p, { body, query, timeoutMs } = {}) {
|
|
328
|
+
const address = this.address;
|
|
329
|
+
if (!address)
|
|
330
|
+
return Promise.reject(new Error("the signer is not started"));
|
|
331
|
+
const params = new URLSearchParams();
|
|
332
|
+
for (const [k, v] of Object.entries(query ?? {}))
|
|
333
|
+
if (v !== undefined)
|
|
334
|
+
params.set(k, String(v));
|
|
335
|
+
const queryText = params.toString();
|
|
336
|
+
const data = body === undefined ? undefined : JSON.stringify(body);
|
|
337
|
+
const headers = { Accept: "application/json" };
|
|
338
|
+
if (data !== undefined)
|
|
339
|
+
headers["Content-Type"] = "application/json";
|
|
340
|
+
if (this.token)
|
|
341
|
+
headers["Authorization"] = "Bearer " + this.token;
|
|
342
|
+
const target = address.startsWith("unix:")
|
|
343
|
+
? { socketPath: address.slice("unix:".length) }
|
|
344
|
+
: { host: address.slice(0, address.lastIndexOf(":")), port: Number(address.slice(address.lastIndexOf(":") + 1)) };
|
|
345
|
+
return new Promise((resolve, reject) => {
|
|
346
|
+
const req = http.request({ ...target, method, path: p + (queryText ? "?" + queryText : ""), headers, timeout: timeoutMs ?? this.options.timeoutMs ?? 40_000 }, (res) => {
|
|
347
|
+
const chunks = [];
|
|
348
|
+
res.on("data", (c) => chunks.push(c));
|
|
349
|
+
res.on("end", () => {
|
|
350
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
351
|
+
if ((res.statusCode ?? 0) >= 400) {
|
|
352
|
+
let code = "http_error";
|
|
353
|
+
let message = res.statusMessage || text.slice(0, 200);
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(text);
|
|
356
|
+
if (typeof parsed.code === "string")
|
|
357
|
+
code = parsed.code;
|
|
358
|
+
if (typeof parsed.message === "string")
|
|
359
|
+
message = parsed.message;
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// not JSON: keep the text
|
|
363
|
+
}
|
|
364
|
+
reject(new EngineError(res.statusCode ?? 0, code, message));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
resolve(text ? JSON.parse(text) : null);
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
reject(err);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
req.on("timeout", () => req.destroy(new Error("the signer did not answer in time")));
|
|
376
|
+
req.on("error", reject);
|
|
377
|
+
if (data !== undefined)
|
|
378
|
+
req.write(data);
|
|
379
|
+
req.end();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function freePort() {
|
|
384
|
+
return new Promise((resolve, reject) => {
|
|
385
|
+
const srv = net.createServer();
|
|
386
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
387
|
+
const port = srv.address().port;
|
|
388
|
+
srv.close(() => resolve(port));
|
|
389
|
+
});
|
|
390
|
+
srv.on("error", reject);
|
|
391
|
+
});
|
|
392
|
+
}
|
package/dist/time.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Times as the contract writes them. */
|
|
2
|
+
/** Format a time as the contract wants it: UTC, Z suffix, fraction trimmed. */
|
|
3
|
+
export declare function rfc3339(d: Date): string;
|
|
4
|
+
/** A Date from an RFC 3339 string that carries an offset. */
|
|
5
|
+
export declare function parseTime(s: string): Date;
|
package/dist/time.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Times as the contract writes them. */
|
|
2
|
+
/** Format a time as the contract wants it: UTC, Z suffix, fraction trimmed. */
|
|
3
|
+
export function rfc3339(d) {
|
|
4
|
+
const iso = d.toISOString(); // 2026-10-18T10:00:00.250Z
|
|
5
|
+
const [whole, rest] = iso.split(".");
|
|
6
|
+
const fraction = (rest ?? "").replace(/Z$/, "").replace(/0+$/, "");
|
|
7
|
+
return fraction ? `${whole}.${fraction}Z` : `${whole}Z`;
|
|
8
|
+
}
|
|
9
|
+
/** A Date from an RFC 3339 string that carries an offset. */
|
|
10
|
+
export function parseTime(s) {
|
|
11
|
+
if (!/(Z|[+-]\d\d:\d\d)$/.test(s))
|
|
12
|
+
throw new Error(`time ${JSON.stringify(s)} has no offset`);
|
|
13
|
+
const d = new Date(s);
|
|
14
|
+
if (Number.isNaN(d.getTime()))
|
|
15
|
+
throw new Error(`not a time: ${JSON.stringify(s)}`);
|
|
16
|
+
return d;
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mishmar",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Mishmar client: sign and submit proposals to the engine that guards your trading agent",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "node --test \"tests/**/*.test.ts\""
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^24.3.0",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/mishmar-io/mishmar.git",
|
|
32
|
+
"directory": "typescript"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://mishmar.io",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"trading",
|
|
37
|
+
"agents",
|
|
38
|
+
"risk",
|
|
39
|
+
"guardrails",
|
|
40
|
+
"hyperliquid",
|
|
41
|
+
"binance"
|
|
42
|
+
]
|
|
43
|
+
}
|