@routedock/routedock 0.3.0 → 0.4.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 +113 -12
- package/dist/{MppSessionHandler-1EL8QBAn.d.cts → MppSessionHandler-3QfCHI5x.d.ts} +40 -2
- package/dist/{MppSessionHandler-Bq4S-JTE.d.ts → MppSessionHandler-xu4Kxu6N.d.cts} +40 -2
- package/dist/{ProviderRegistry-CA_TdREG.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
- package/dist/{ProviderRegistry-D-0rqK0N.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
- package/dist/{RouteDockClient-go9aopfF.d.cts → RouteDockClient-6rmgkbYb.d.cts} +124 -142
- package/dist/{RouteDockClient-Bh86bVly.d.ts → RouteDockClient-CnAkxwrW.d.ts} +124 -142
- package/dist/{SessionReconciler-Bw-PoBVl.d.ts → SessionReconciler-Cce9MXO1.d.ts} +10 -3
- package/dist/{SessionReconciler-Cz2nuOlJ.d.cts → SessionReconciler-Dpe9qXdD.d.cts} +10 -3
- package/dist/{SessionStore-BaYiegP0.d.ts → SessionStore-DkLBxFQu.d.cts} +28 -3
- package/dist/{SessionStore-B4-Hi4H8.d.cts → SessionStore-_ZkoBzFY.d.ts} +28 -3
- package/dist/SpendStore-BUclV02i.d.ts +73 -0
- package/dist/SpendStore-s_dfcemj.d.cts +73 -0
- package/dist/client.cjs +767 -487
- package/dist/client.d.cts +80 -10
- package/dist/client.d.ts +80 -10
- package/dist/client.js +733 -468
- package/dist/index.cjs +1295 -590
- package/dist/index.d.cts +13 -10
- package/dist/index.d.ts +13 -10
- package/dist/index.js +1247 -559
- package/dist/logger-DVa7VT4c.d.cts +41 -0
- package/dist/logger-DVa7VT4c.d.ts +41 -0
- package/dist/network.cjs +57 -0
- package/dist/network.d.cts +72 -0
- package/dist/network.d.ts +72 -0
- package/dist/network.js +49 -0
- package/dist/node.cjs +166 -0
- package/dist/node.d.cts +77 -0
- package/dist/node.d.ts +77 -0
- package/dist/node.js +159 -0
- package/dist/provider/fastify.cjs +847 -62
- package/dist/provider/fastify.d.cts +10 -3
- package/dist/provider/fastify.d.ts +10 -3
- package/dist/provider/fastify.js +848 -63
- package/dist/provider/hono.cjs +936 -60
- package/dist/provider/hono.d.cts +23 -8
- package/dist/provider/hono.d.ts +23 -8
- package/dist/provider/hono.js +937 -61
- package/dist/provider.cjs +894 -70
- package/dist/provider.d.cts +9 -8
- package/dist/provider.d.ts +9 -8
- package/dist/provider.js +895 -72
- package/dist/react.cjs +736 -436
- package/dist/react.d.cts +7 -5
- package/dist/react.d.ts +7 -5
- package/dist/react.js +725 -425
- package/dist/{routedockMiddleware-57gUaEMf.d.ts → routedockMiddleware-Cq9HOJEa.d.ts} +9 -2
- package/dist/{routedockMiddleware-DlyLrxp7.d.cts → routedockMiddleware-CxsWkSKa.d.cts} +9 -2
- package/dist/schema.cjs +7 -0
- package/dist/schema.js +7 -0
- package/dist/{sign-AKH67sft.d.cts → sign-Bq08SeFq.d.ts} +2 -2
- package/dist/{sign-BEq-OsSq.d.ts → sign-DqGE0Plv.d.cts} +2 -2
- package/dist/testing.d.cts +4 -3
- package/dist/testing.d.ts +4 -3
- package/dist/{logger-D8_Q2ytp.d.cts → types-DXaS5Wip.d.cts} +85 -47
- package/dist/{logger-D8_Q2ytp.d.ts → types-DXaS5Wip.d.ts} +85 -47
- package/package.json +50 -4
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging hook shared by the client and provider adapters.
|
|
3
|
+
*
|
|
4
|
+
* The SDK used to write straight to `console` in a couple of dozen error
|
|
5
|
+
* paths, which is unusable on structured-logging runtimes (Cloudflare Workers,
|
|
6
|
+
* Datadog, …) and untestable without spying on `console`. Every internal
|
|
7
|
+
* diagnostic now goes through a {@link RouteDockLogger}, so a consumer can
|
|
8
|
+
* silence it, redirect it, or attach correlation fields.
|
|
9
|
+
*
|
|
10
|
+
* The default implementation is console-backed, so behaviour is unchanged when
|
|
11
|
+
* no logger is supplied.
|
|
12
|
+
*/
|
|
13
|
+
/** Severity of a log line. */
|
|
14
|
+
type RouteDockLogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
15
|
+
/**
|
|
16
|
+
* Structured context attached to a log line, typically `{ error: err }`. Sinks
|
|
17
|
+
* receive the values unresolved, so a serialising sink must project non-JSON
|
|
18
|
+
* values such as `Error` itself.
|
|
19
|
+
*/
|
|
20
|
+
type RouteDockLogFields = Record<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Consumer-supplied log sink.
|
|
23
|
+
*
|
|
24
|
+
* `fields` carries the structured payload (the error, the channel id, …) that
|
|
25
|
+
* a bare string would drop at the boundary.
|
|
26
|
+
*/
|
|
27
|
+
type RouteDockLogger = (level: RouteDockLogLevel, message: string, fields?: RouteDockLogFields) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Default logger that forwards to the console at the matching level. Field
|
|
30
|
+
* values are spread as additional console arguments so `console.error(msg, err)`
|
|
31
|
+
* spies (and existing log shippers) keep seeing the raw error object.
|
|
32
|
+
*/
|
|
33
|
+
declare function createConsoleLogger(): RouteDockLogger;
|
|
34
|
+
/** Shared console-backed logger — the default when none is configured. */
|
|
35
|
+
declare const consoleLogger: RouteDockLogger;
|
|
36
|
+
/** Logger that discards every line. */
|
|
37
|
+
declare const noopLogger: RouteDockLogger;
|
|
38
|
+
/** Resolve an optional logger to a concrete sink, defaulting to `fallback`. */
|
|
39
|
+
declare function resolveLogger(logger: RouteDockLogger | undefined, fallback?: RouteDockLogger): RouteDockLogger;
|
|
40
|
+
|
|
41
|
+
export { type RouteDockLogFields as R, type RouteDockLogLevel as a, type RouteDockLogger as b, consoleLogger as c, createConsoleLogger as d, noopLogger as n, resolveLogger as r };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging hook shared by the client and provider adapters.
|
|
3
|
+
*
|
|
4
|
+
* The SDK used to write straight to `console` in a couple of dozen error
|
|
5
|
+
* paths, which is unusable on structured-logging runtimes (Cloudflare Workers,
|
|
6
|
+
* Datadog, …) and untestable without spying on `console`. Every internal
|
|
7
|
+
* diagnostic now goes through a {@link RouteDockLogger}, so a consumer can
|
|
8
|
+
* silence it, redirect it, or attach correlation fields.
|
|
9
|
+
*
|
|
10
|
+
* The default implementation is console-backed, so behaviour is unchanged when
|
|
11
|
+
* no logger is supplied.
|
|
12
|
+
*/
|
|
13
|
+
/** Severity of a log line. */
|
|
14
|
+
type RouteDockLogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
15
|
+
/**
|
|
16
|
+
* Structured context attached to a log line, typically `{ error: err }`. Sinks
|
|
17
|
+
* receive the values unresolved, so a serialising sink must project non-JSON
|
|
18
|
+
* values such as `Error` itself.
|
|
19
|
+
*/
|
|
20
|
+
type RouteDockLogFields = Record<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Consumer-supplied log sink.
|
|
23
|
+
*
|
|
24
|
+
* `fields` carries the structured payload (the error, the channel id, …) that
|
|
25
|
+
* a bare string would drop at the boundary.
|
|
26
|
+
*/
|
|
27
|
+
type RouteDockLogger = (level: RouteDockLogLevel, message: string, fields?: RouteDockLogFields) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Default logger that forwards to the console at the matching level. Field
|
|
30
|
+
* values are spread as additional console arguments so `console.error(msg, err)`
|
|
31
|
+
* spies (and existing log shippers) keep seeing the raw error object.
|
|
32
|
+
*/
|
|
33
|
+
declare function createConsoleLogger(): RouteDockLogger;
|
|
34
|
+
/** Shared console-backed logger — the default when none is configured. */
|
|
35
|
+
declare const consoleLogger: RouteDockLogger;
|
|
36
|
+
/** Logger that discards every line. */
|
|
37
|
+
declare const noopLogger: RouteDockLogger;
|
|
38
|
+
/** Resolve an optional logger to a concrete sink, defaulting to `fallback`. */
|
|
39
|
+
declare function resolveLogger(logger: RouteDockLogger | undefined, fallback?: RouteDockLogger): RouteDockLogger;
|
|
40
|
+
|
|
41
|
+
export { type RouteDockLogFields as R, type RouteDockLogLevel as a, type RouteDockLogger as b, consoleLogger as c, createConsoleLogger as d, noopLogger as n, resolveLogger as r };
|
package/dist/network.cjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/network.ts
|
|
4
|
+
var USDC_ISSUERS = {
|
|
5
|
+
testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
|
|
6
|
+
mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
|
|
7
|
+
};
|
|
8
|
+
var TESTNET_USDC_CONTRACT = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
|
|
9
|
+
var HORIZON_URLS = {
|
|
10
|
+
testnet: "https://horizon-testnet.stellar.org",
|
|
11
|
+
mainnet: "https://horizon.stellar.org"
|
|
12
|
+
};
|
|
13
|
+
function resolveNetwork(raw) {
|
|
14
|
+
return raw === "mainnet" ? "mainnet" : "testnet";
|
|
15
|
+
}
|
|
16
|
+
function resolveAssetContract(env, network) {
|
|
17
|
+
if (env.USDC_ASSET_CONTRACT) return env.USDC_ASSET_CONTRACT;
|
|
18
|
+
if (network === "testnet") return TESTNET_USDC_CONTRACT;
|
|
19
|
+
throw new Error("USDC_ASSET_CONTRACT is required for mainnet");
|
|
20
|
+
}
|
|
21
|
+
function findStellarConfigProblems(env, network) {
|
|
22
|
+
const problems = [];
|
|
23
|
+
if (!env.STELLAR_PAYEE_SECRET || !env.STELLAR_PAYEE_SECRET.startsWith("S")) {
|
|
24
|
+
problems.push("STELLAR_PAYEE_SECRET");
|
|
25
|
+
}
|
|
26
|
+
if (!env.STELLAR_PAYEE_ADDRESS) {
|
|
27
|
+
problems.push("STELLAR_PAYEE_ADDRESS");
|
|
28
|
+
}
|
|
29
|
+
if (network === "mainnet" && !env.USDC_ASSET_CONTRACT) {
|
|
30
|
+
problems.push("USDC_ASSET_CONTRACT");
|
|
31
|
+
}
|
|
32
|
+
return problems;
|
|
33
|
+
}
|
|
34
|
+
async function fetchXlmUsdcOrderBook(network, limit) {
|
|
35
|
+
const params = new URLSearchParams({
|
|
36
|
+
selling_asset_type: "native",
|
|
37
|
+
buying_asset_type: "credit_alphanum4",
|
|
38
|
+
buying_asset_code: "USDC",
|
|
39
|
+
buying_asset_issuer: USDC_ISSUERS[network],
|
|
40
|
+
limit: String(limit)
|
|
41
|
+
});
|
|
42
|
+
const response = await fetch(`${HORIZON_URLS[network]}/order_book?${params.toString()}`, {
|
|
43
|
+
headers: { accept: "application/json" }
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Horizon responded ${response.status}`);
|
|
47
|
+
}
|
|
48
|
+
return await response.json();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
exports.HORIZON_URLS = HORIZON_URLS;
|
|
52
|
+
exports.TESTNET_USDC_CONTRACT = TESTNET_USDC_CONTRACT;
|
|
53
|
+
exports.USDC_ISSUERS = USDC_ISSUERS;
|
|
54
|
+
exports.fetchXlmUsdcOrderBook = fetchXlmUsdcOrderBook;
|
|
55
|
+
exports.findStellarConfigProblems = findStellarConfigProblems;
|
|
56
|
+
exports.resolveAssetContract = resolveAssetContract;
|
|
57
|
+
exports.resolveNetwork = resolveNetwork;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Stellar network constants and the small helpers shared by the
|
|
3
|
+
* client, the provider middleware and the provider apps.
|
|
4
|
+
*
|
|
5
|
+
* Kept in its own entry point (`@routedock/routedock/network`) so Workers
|
|
6
|
+
* bundles can import it without pulling in the whole client or provider
|
|
7
|
+
* surface: plain constants plus one fetch-based helper, no Node APIs, no
|
|
8
|
+
* crypto. Before this module existed each app kept its own copy of the USDC
|
|
9
|
+
* issuer table, the testnet SAC and the Horizon URLs, and a mainnet change
|
|
10
|
+
* had to be made in three places (issue #578).
|
|
11
|
+
*/
|
|
12
|
+
/** Canonical Circle USDC issuers keyed by Stellar network. */
|
|
13
|
+
declare const USDC_ISSUERS: {
|
|
14
|
+
readonly testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
|
|
15
|
+
readonly mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
|
|
16
|
+
};
|
|
17
|
+
type Network = keyof typeof USDC_ISSUERS;
|
|
18
|
+
/** Well-known USDC stablecoin asset contract on testnet. */
|
|
19
|
+
declare const TESTNET_USDC_CONTRACT = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
|
|
20
|
+
/** Horizon REST endpoints keyed by Stellar network. */
|
|
21
|
+
declare const HORIZON_URLS: {
|
|
22
|
+
readonly testnet: "https://horizon-testnet.stellar.org";
|
|
23
|
+
readonly mainnet: "https://horizon.stellar.org";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a STELLAR_NETWORK-style string to a {@link Network}.
|
|
27
|
+
* Anything other than the exact string 'mainnet' falls back to testnet, the
|
|
28
|
+
* safe default.
|
|
29
|
+
*/
|
|
30
|
+
declare function resolveNetwork(raw: string | undefined): Network;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the USDC asset contract for `network`: an explicit
|
|
33
|
+
* `USDC_ASSET_CONTRACT` always wins, testnet falls back to the well-known
|
|
34
|
+
* testnet SAC, and mainnet without one configured is a hard failure rather
|
|
35
|
+
* than a silently empty asset contract in the manifest.
|
|
36
|
+
*/
|
|
37
|
+
declare function resolveAssetContract(env: {
|
|
38
|
+
USDC_ASSET_CONTRACT?: string | undefined;
|
|
39
|
+
}, network: Network): string;
|
|
40
|
+
/** The env fields the shared config check understands. */
|
|
41
|
+
interface StellarConfigEnv {
|
|
42
|
+
STELLAR_PAYEE_SECRET?: string | undefined;
|
|
43
|
+
STELLAR_PAYEE_ADDRESS?: string | undefined;
|
|
44
|
+
USDC_ASSET_CONTRACT?: string | undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Return the names of environment variables that are unset or malformed.
|
|
48
|
+
* Values are never included — only names, so this is safe to log or return
|
|
49
|
+
* to a caller directly. Providers append their own extra requirements
|
|
50
|
+
* (channel contract, commitment key) to this shared base list.
|
|
51
|
+
*/
|
|
52
|
+
declare function findStellarConfigProblems(env: StellarConfigEnv, network: Network): string[];
|
|
53
|
+
interface OrderBookLevel {
|
|
54
|
+
price: string;
|
|
55
|
+
amount: string;
|
|
56
|
+
}
|
|
57
|
+
interface XlmUsdcOrderBook {
|
|
58
|
+
asks: OrderBookLevel[];
|
|
59
|
+
bids: OrderBookLevel[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Fetch the XLM/USDC orderbook straight from Horizon's REST API.
|
|
63
|
+
*
|
|
64
|
+
* The provider Express build used `Horizon.Server(...).orderbook(...)`, which
|
|
65
|
+
* pulls the whole `@stellar/stellar-sdk` (and axios) into the bundle for what
|
|
66
|
+
* is a single GET. Calling the endpoint keeps Workers bundles small and drops
|
|
67
|
+
* the dependency, while the issuer and Horizon URL come from the canonical
|
|
68
|
+
* tables above.
|
|
69
|
+
*/
|
|
70
|
+
declare function fetchXlmUsdcOrderBook(network: Network, limit: number): Promise<XlmUsdcOrderBook>;
|
|
71
|
+
|
|
72
|
+
export { HORIZON_URLS, type Network, type OrderBookLevel, type StellarConfigEnv, TESTNET_USDC_CONTRACT, USDC_ISSUERS, type XlmUsdcOrderBook, fetchXlmUsdcOrderBook, findStellarConfigProblems, resolveAssetContract, resolveNetwork };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Stellar network constants and the small helpers shared by the
|
|
3
|
+
* client, the provider middleware and the provider apps.
|
|
4
|
+
*
|
|
5
|
+
* Kept in its own entry point (`@routedock/routedock/network`) so Workers
|
|
6
|
+
* bundles can import it without pulling in the whole client or provider
|
|
7
|
+
* surface: plain constants plus one fetch-based helper, no Node APIs, no
|
|
8
|
+
* crypto. Before this module existed each app kept its own copy of the USDC
|
|
9
|
+
* issuer table, the testnet SAC and the Horizon URLs, and a mainnet change
|
|
10
|
+
* had to be made in three places (issue #578).
|
|
11
|
+
*/
|
|
12
|
+
/** Canonical Circle USDC issuers keyed by Stellar network. */
|
|
13
|
+
declare const USDC_ISSUERS: {
|
|
14
|
+
readonly testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
|
|
15
|
+
readonly mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
|
|
16
|
+
};
|
|
17
|
+
type Network = keyof typeof USDC_ISSUERS;
|
|
18
|
+
/** Well-known USDC stablecoin asset contract on testnet. */
|
|
19
|
+
declare const TESTNET_USDC_CONTRACT = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
|
|
20
|
+
/** Horizon REST endpoints keyed by Stellar network. */
|
|
21
|
+
declare const HORIZON_URLS: {
|
|
22
|
+
readonly testnet: "https://horizon-testnet.stellar.org";
|
|
23
|
+
readonly mainnet: "https://horizon.stellar.org";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a STELLAR_NETWORK-style string to a {@link Network}.
|
|
27
|
+
* Anything other than the exact string 'mainnet' falls back to testnet, the
|
|
28
|
+
* safe default.
|
|
29
|
+
*/
|
|
30
|
+
declare function resolveNetwork(raw: string | undefined): Network;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the USDC asset contract for `network`: an explicit
|
|
33
|
+
* `USDC_ASSET_CONTRACT` always wins, testnet falls back to the well-known
|
|
34
|
+
* testnet SAC, and mainnet without one configured is a hard failure rather
|
|
35
|
+
* than a silently empty asset contract in the manifest.
|
|
36
|
+
*/
|
|
37
|
+
declare function resolveAssetContract(env: {
|
|
38
|
+
USDC_ASSET_CONTRACT?: string | undefined;
|
|
39
|
+
}, network: Network): string;
|
|
40
|
+
/** The env fields the shared config check understands. */
|
|
41
|
+
interface StellarConfigEnv {
|
|
42
|
+
STELLAR_PAYEE_SECRET?: string | undefined;
|
|
43
|
+
STELLAR_PAYEE_ADDRESS?: string | undefined;
|
|
44
|
+
USDC_ASSET_CONTRACT?: string | undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Return the names of environment variables that are unset or malformed.
|
|
48
|
+
* Values are never included — only names, so this is safe to log or return
|
|
49
|
+
* to a caller directly. Providers append their own extra requirements
|
|
50
|
+
* (channel contract, commitment key) to this shared base list.
|
|
51
|
+
*/
|
|
52
|
+
declare function findStellarConfigProblems(env: StellarConfigEnv, network: Network): string[];
|
|
53
|
+
interface OrderBookLevel {
|
|
54
|
+
price: string;
|
|
55
|
+
amount: string;
|
|
56
|
+
}
|
|
57
|
+
interface XlmUsdcOrderBook {
|
|
58
|
+
asks: OrderBookLevel[];
|
|
59
|
+
bids: OrderBookLevel[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Fetch the XLM/USDC orderbook straight from Horizon's REST API.
|
|
63
|
+
*
|
|
64
|
+
* The provider Express build used `Horizon.Server(...).orderbook(...)`, which
|
|
65
|
+
* pulls the whole `@stellar/stellar-sdk` (and axios) into the bundle for what
|
|
66
|
+
* is a single GET. Calling the endpoint keeps Workers bundles small and drops
|
|
67
|
+
* the dependency, while the issuer and Horizon URL come from the canonical
|
|
68
|
+
* tables above.
|
|
69
|
+
*/
|
|
70
|
+
declare function fetchXlmUsdcOrderBook(network: Network, limit: number): Promise<XlmUsdcOrderBook>;
|
|
71
|
+
|
|
72
|
+
export { HORIZON_URLS, type Network, type OrderBookLevel, type StellarConfigEnv, TESTNET_USDC_CONTRACT, USDC_ISSUERS, type XlmUsdcOrderBook, fetchXlmUsdcOrderBook, findStellarConfigProblems, resolveAssetContract, resolveNetwork };
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/network.ts
|
|
2
|
+
var USDC_ISSUERS = {
|
|
3
|
+
testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
|
|
4
|
+
mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
|
|
5
|
+
};
|
|
6
|
+
var TESTNET_USDC_CONTRACT = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
|
|
7
|
+
var HORIZON_URLS = {
|
|
8
|
+
testnet: "https://horizon-testnet.stellar.org",
|
|
9
|
+
mainnet: "https://horizon.stellar.org"
|
|
10
|
+
};
|
|
11
|
+
function resolveNetwork(raw) {
|
|
12
|
+
return raw === "mainnet" ? "mainnet" : "testnet";
|
|
13
|
+
}
|
|
14
|
+
function resolveAssetContract(env, network) {
|
|
15
|
+
if (env.USDC_ASSET_CONTRACT) return env.USDC_ASSET_CONTRACT;
|
|
16
|
+
if (network === "testnet") return TESTNET_USDC_CONTRACT;
|
|
17
|
+
throw new Error("USDC_ASSET_CONTRACT is required for mainnet");
|
|
18
|
+
}
|
|
19
|
+
function findStellarConfigProblems(env, network) {
|
|
20
|
+
const problems = [];
|
|
21
|
+
if (!env.STELLAR_PAYEE_SECRET || !env.STELLAR_PAYEE_SECRET.startsWith("S")) {
|
|
22
|
+
problems.push("STELLAR_PAYEE_SECRET");
|
|
23
|
+
}
|
|
24
|
+
if (!env.STELLAR_PAYEE_ADDRESS) {
|
|
25
|
+
problems.push("STELLAR_PAYEE_ADDRESS");
|
|
26
|
+
}
|
|
27
|
+
if (network === "mainnet" && !env.USDC_ASSET_CONTRACT) {
|
|
28
|
+
problems.push("USDC_ASSET_CONTRACT");
|
|
29
|
+
}
|
|
30
|
+
return problems;
|
|
31
|
+
}
|
|
32
|
+
async function fetchXlmUsdcOrderBook(network, limit) {
|
|
33
|
+
const params = new URLSearchParams({
|
|
34
|
+
selling_asset_type: "native",
|
|
35
|
+
buying_asset_type: "credit_alphanum4",
|
|
36
|
+
buying_asset_code: "USDC",
|
|
37
|
+
buying_asset_issuer: USDC_ISSUERS[network],
|
|
38
|
+
limit: String(limit)
|
|
39
|
+
});
|
|
40
|
+
const response = await fetch(`${HORIZON_URLS[network]}/order_book?${params.toString()}`, {
|
|
41
|
+
headers: { accept: "application/json" }
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Horizon responded ${response.status}`);
|
|
45
|
+
}
|
|
46
|
+
return await response.json();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { HORIZON_URLS, TESTNET_USDC_CONTRACT, USDC_ISSUERS, fetchXlmUsdcOrderBook, findStellarConfigProblems, resolveAssetContract, resolveNetwork };
|
package/dist/node.cjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var fs = require('fs/promises');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
|
|
7
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
10
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
11
|
+
|
|
12
|
+
// src/store/FileSpendStore.ts
|
|
13
|
+
var DEFAULT_LOCK_STALE_MS = 1e4;
|
|
14
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 15e3;
|
|
15
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
var FileSpendStore = class {
|
|
17
|
+
filePath;
|
|
18
|
+
lockPath;
|
|
19
|
+
lockStaleMs;
|
|
20
|
+
lockTimeoutMs;
|
|
21
|
+
constructor(filePath, options = {}) {
|
|
22
|
+
this.filePath = filePath;
|
|
23
|
+
this.lockPath = `${filePath}.lock`;
|
|
24
|
+
this.lockStaleMs = options.lockStaleMs ?? DEFAULT_LOCK_STALE_MS;
|
|
25
|
+
this.lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
26
|
+
}
|
|
27
|
+
async read() {
|
|
28
|
+
let data;
|
|
29
|
+
try {
|
|
30
|
+
data = await fs__default.default.readFile(this.filePath, "utf-8");
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (err.code === "ENOENT") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[FileSpendStore] Failed to read spend file at ${this.filePath}: ${err.message}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(data);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`[FileSpendStore] Spend file at ${this.filePath} contains invalid JSON. Delete or repair it before resuming. Raw content: ${data.slice(0, 120)}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async write(state) {
|
|
48
|
+
await this.withLock(async (assertHeld) => {
|
|
49
|
+
await this.writeFile(state, assertHeld);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Atomically read-modify-write the accumulator under the cross-process lock.
|
|
54
|
+
* `fn` receives the current state (or null) and returns the state to persist,
|
|
55
|
+
* or `undefined` to leave the file untouched. If `fn` throws, nothing is
|
|
56
|
+
* written and the lock is released.
|
|
57
|
+
*/
|
|
58
|
+
async update(fn) {
|
|
59
|
+
return this.withLock(async (assertHeld) => {
|
|
60
|
+
const prev = await this.read();
|
|
61
|
+
const next = fn(prev);
|
|
62
|
+
if (next === void 0) return prev;
|
|
63
|
+
await this.writeFile(next, assertHeld);
|
|
64
|
+
return next;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// ── internals ──────────────────────────────────────────────────────────────
|
|
68
|
+
async writeFile(state, assertHeld) {
|
|
69
|
+
const tmpPath = `${this.filePath}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
70
|
+
try {
|
|
71
|
+
await fs__default.default.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
72
|
+
await assertHeld();
|
|
73
|
+
await fs__default.default.rename(tmpPath, this.filePath);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
await fs__default.default.rm(tmpPath, { force: true }).catch(() => {
|
|
76
|
+
});
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Run `fn` while holding the lock file; always releases it. */
|
|
81
|
+
async withLock(fn) {
|
|
82
|
+
await fs__default.default.mkdir(path__default.default.dirname(this.filePath), { recursive: true });
|
|
83
|
+
const token = `${process.pid}:${crypto.randomBytes(8).toString("hex")}`;
|
|
84
|
+
await this.acquireLock(token);
|
|
85
|
+
const heartbeat = setInterval(() => {
|
|
86
|
+
const now = /* @__PURE__ */ new Date();
|
|
87
|
+
fs__default.default.utimes(this.lockPath, now, now).catch(() => {
|
|
88
|
+
});
|
|
89
|
+
}, Math.max(10, Math.floor(this.lockStaleMs / 3)));
|
|
90
|
+
heartbeat.unref();
|
|
91
|
+
const assertHeld = async () => {
|
|
92
|
+
let current;
|
|
93
|
+
try {
|
|
94
|
+
current = await fs__default.default.readFile(this.lockPath, "utf-8");
|
|
95
|
+
} catch {
|
|
96
|
+
current = "";
|
|
97
|
+
}
|
|
98
|
+
if (current !== token) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`[FileSpendStore] Lost the lock on ${this.lockPath} before committing; refusing to overwrite the spend file. Retry the operation.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
return await fn(assertHeld);
|
|
106
|
+
} finally {
|
|
107
|
+
clearInterval(heartbeat);
|
|
108
|
+
await this.releaseLock(token);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async acquireLock(token) {
|
|
112
|
+
const deadline = Date.now() + this.lockTimeoutMs;
|
|
113
|
+
let delay = 5;
|
|
114
|
+
for (; ; ) {
|
|
115
|
+
try {
|
|
116
|
+
const handle = await fs__default.default.open(this.lockPath, "wx");
|
|
117
|
+
try {
|
|
118
|
+
await handle.writeFile(token, "utf-8");
|
|
119
|
+
} finally {
|
|
120
|
+
await handle.close();
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err.code !== "EEXIST") {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`[FileSpendStore] Failed to create lock file ${this.lockPath}: ${err.message}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
await this.breakIfStale();
|
|
131
|
+
if (Date.now() >= deadline) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`[FileSpendStore] Timed out after ${this.lockTimeoutMs}ms waiting for ${this.lockPath}. Another process is holding the spend file lock; if none is running, delete the lock file.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
await sleep(delay + Math.floor(Math.random() * delay));
|
|
137
|
+
delay = Math.min(delay * 2, 50);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Remove the lock if its holder stopped refreshing it (crashed process). */
|
|
141
|
+
async breakIfStale() {
|
|
142
|
+
let before;
|
|
143
|
+
try {
|
|
144
|
+
before = await fs__default.default.stat(this.lockPath);
|
|
145
|
+
} catch {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (Date.now() - before.mtimeMs <= this.lockStaleMs) return;
|
|
149
|
+
try {
|
|
150
|
+
const after = await fs__default.default.stat(this.lockPath);
|
|
151
|
+
if (after.ino !== before.ino || after.mtimeMs !== before.mtimeMs) return;
|
|
152
|
+
await fs__default.default.unlink(this.lockPath);
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async releaseLock(token) {
|
|
157
|
+
try {
|
|
158
|
+
if (await fs__default.default.readFile(this.lockPath, "utf-8") === token) {
|
|
159
|
+
await fs__default.default.unlink(this.lockPath);
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
exports.FileSpendStore = FileSpendStore;
|
package/dist/node.d.cts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { S as SpendStore, D as DailySpend } from './SpendStore-s_dfcemj.cjs';
|
|
2
|
+
import './logger-DVa7VT4c.cjs';
|
|
3
|
+
|
|
4
|
+
/** Tuning for the cross-process lock that guards read-modify-write. */
|
|
5
|
+
interface FileSpendStoreOptions {
|
|
6
|
+
/**
|
|
7
|
+
* A lock file whose mtime is older than this is treated as left behind by a
|
|
8
|
+
* crashed process and broken. A live holder refreshes it, so this only needs
|
|
9
|
+
* to exceed the refresh interval. Default 10 000 ms.
|
|
10
|
+
*/
|
|
11
|
+
lockStaleMs?: number;
|
|
12
|
+
/**
|
|
13
|
+
* How long to wait for the lock before giving up. A spend cap must fail
|
|
14
|
+
* closed, so a timeout rejects instead of proceeding unlocked. Must be longer
|
|
15
|
+
* than `lockStaleMs` to survive a crashed holder. Default 15 000 ms.
|
|
16
|
+
*/
|
|
17
|
+
lockTimeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* File-backed durable {@link SpendStore}.
|
|
21
|
+
*
|
|
22
|
+
* Persists the daily spend accumulator to a JSON file on disk, preserving
|
|
23
|
+
* cumulative spending totals across process restarts, crashes, and redeploys.
|
|
24
|
+
*
|
|
25
|
+
* ### Error semantics
|
|
26
|
+
* - File absent (ENOENT): returns null — legitimately no spend recorded yet.
|
|
27
|
+
* - File present but unreadable or malformed: throws — a safety control must
|
|
28
|
+
* fail closed, not assume nothing was spent.
|
|
29
|
+
*
|
|
30
|
+
* ### Write atomicity
|
|
31
|
+
* Writes go to a tmp file that is unique per write (`<filePath>.<pid>.<random>.tmp`),
|
|
32
|
+
* then `fs.rename` onto the target. `rename` is atomic within a filesystem, so
|
|
33
|
+
* there is no window where the target file is truncated but not yet fully
|
|
34
|
+
* written, and two writers can never share (and clobber, or `ENOENT` on) one
|
|
35
|
+
* tmp file.
|
|
36
|
+
*
|
|
37
|
+
* ### Sharing the file between processes
|
|
38
|
+
* `read()` + `write()` alone is not enough when several processes use the same
|
|
39
|
+
* file (the MCP server and the E2E agent both default to
|
|
40
|
+
* `~/.routedock/spend.json`): both can read the same total and both write back
|
|
41
|
+
* `total + amount`. {@link FileSpendStore.update} closes that race — it holds an
|
|
42
|
+
* exclusive lock file (`<filePath>.lock`, created with `O_EXCL`) across the
|
|
43
|
+
* read, the callback and the write, and `RouteDockClient` uses it automatically.
|
|
44
|
+
* `write()` takes the same lock, so every mutation is serialized.
|
|
45
|
+
*
|
|
46
|
+
* The lock is advisory and file-based: it works for processes on one machine
|
|
47
|
+
* (and on filesystems with working `O_EXCL`), not for a file on a flaky network
|
|
48
|
+
* share. A lock whose mtime goes stale (crashed holder) is broken after
|
|
49
|
+
* `lockStaleMs`; a live holder keeps refreshing it. If the lock cannot be
|
|
50
|
+
* acquired within `lockTimeoutMs`, the call rejects — it never proceeds
|
|
51
|
+
* unlocked.
|
|
52
|
+
*/
|
|
53
|
+
declare class FileSpendStore implements SpendStore {
|
|
54
|
+
readonly filePath: string;
|
|
55
|
+
private readonly lockPath;
|
|
56
|
+
private readonly lockStaleMs;
|
|
57
|
+
private readonly lockTimeoutMs;
|
|
58
|
+
constructor(filePath: string, options?: FileSpendStoreOptions);
|
|
59
|
+
read(): Promise<DailySpend | null>;
|
|
60
|
+
write(state: DailySpend): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Atomically read-modify-write the accumulator under the cross-process lock.
|
|
63
|
+
* `fn` receives the current state (or null) and returns the state to persist,
|
|
64
|
+
* or `undefined` to leave the file untouched. If `fn` throws, nothing is
|
|
65
|
+
* written and the lock is released.
|
|
66
|
+
*/
|
|
67
|
+
update(fn: (prev: DailySpend | null) => DailySpend | undefined): Promise<DailySpend | null>;
|
|
68
|
+
private writeFile;
|
|
69
|
+
/** Run `fn` while holding the lock file; always releases it. */
|
|
70
|
+
private withLock;
|
|
71
|
+
private acquireLock;
|
|
72
|
+
/** Remove the lock if its holder stopped refreshing it (crashed process). */
|
|
73
|
+
private breakIfStale;
|
|
74
|
+
private releaseLock;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { FileSpendStore, type FileSpendStoreOptions };
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { S as SpendStore, D as DailySpend } from './SpendStore-BUclV02i.js';
|
|
2
|
+
import './logger-DVa7VT4c.js';
|
|
3
|
+
|
|
4
|
+
/** Tuning for the cross-process lock that guards read-modify-write. */
|
|
5
|
+
interface FileSpendStoreOptions {
|
|
6
|
+
/**
|
|
7
|
+
* A lock file whose mtime is older than this is treated as left behind by a
|
|
8
|
+
* crashed process and broken. A live holder refreshes it, so this only needs
|
|
9
|
+
* to exceed the refresh interval. Default 10 000 ms.
|
|
10
|
+
*/
|
|
11
|
+
lockStaleMs?: number;
|
|
12
|
+
/**
|
|
13
|
+
* How long to wait for the lock before giving up. A spend cap must fail
|
|
14
|
+
* closed, so a timeout rejects instead of proceeding unlocked. Must be longer
|
|
15
|
+
* than `lockStaleMs` to survive a crashed holder. Default 15 000 ms.
|
|
16
|
+
*/
|
|
17
|
+
lockTimeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* File-backed durable {@link SpendStore}.
|
|
21
|
+
*
|
|
22
|
+
* Persists the daily spend accumulator to a JSON file on disk, preserving
|
|
23
|
+
* cumulative spending totals across process restarts, crashes, and redeploys.
|
|
24
|
+
*
|
|
25
|
+
* ### Error semantics
|
|
26
|
+
* - File absent (ENOENT): returns null — legitimately no spend recorded yet.
|
|
27
|
+
* - File present but unreadable or malformed: throws — a safety control must
|
|
28
|
+
* fail closed, not assume nothing was spent.
|
|
29
|
+
*
|
|
30
|
+
* ### Write atomicity
|
|
31
|
+
* Writes go to a tmp file that is unique per write (`<filePath>.<pid>.<random>.tmp`),
|
|
32
|
+
* then `fs.rename` onto the target. `rename` is atomic within a filesystem, so
|
|
33
|
+
* there is no window where the target file is truncated but not yet fully
|
|
34
|
+
* written, and two writers can never share (and clobber, or `ENOENT` on) one
|
|
35
|
+
* tmp file.
|
|
36
|
+
*
|
|
37
|
+
* ### Sharing the file between processes
|
|
38
|
+
* `read()` + `write()` alone is not enough when several processes use the same
|
|
39
|
+
* file (the MCP server and the E2E agent both default to
|
|
40
|
+
* `~/.routedock/spend.json`): both can read the same total and both write back
|
|
41
|
+
* `total + amount`. {@link FileSpendStore.update} closes that race — it holds an
|
|
42
|
+
* exclusive lock file (`<filePath>.lock`, created with `O_EXCL`) across the
|
|
43
|
+
* read, the callback and the write, and `RouteDockClient` uses it automatically.
|
|
44
|
+
* `write()` takes the same lock, so every mutation is serialized.
|
|
45
|
+
*
|
|
46
|
+
* The lock is advisory and file-based: it works for processes on one machine
|
|
47
|
+
* (and on filesystems with working `O_EXCL`), not for a file on a flaky network
|
|
48
|
+
* share. A lock whose mtime goes stale (crashed holder) is broken after
|
|
49
|
+
* `lockStaleMs`; a live holder keeps refreshing it. If the lock cannot be
|
|
50
|
+
* acquired within `lockTimeoutMs`, the call rejects — it never proceeds
|
|
51
|
+
* unlocked.
|
|
52
|
+
*/
|
|
53
|
+
declare class FileSpendStore implements SpendStore {
|
|
54
|
+
readonly filePath: string;
|
|
55
|
+
private readonly lockPath;
|
|
56
|
+
private readonly lockStaleMs;
|
|
57
|
+
private readonly lockTimeoutMs;
|
|
58
|
+
constructor(filePath: string, options?: FileSpendStoreOptions);
|
|
59
|
+
read(): Promise<DailySpend | null>;
|
|
60
|
+
write(state: DailySpend): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Atomically read-modify-write the accumulator under the cross-process lock.
|
|
63
|
+
* `fn` receives the current state (or null) and returns the state to persist,
|
|
64
|
+
* or `undefined` to leave the file untouched. If `fn` throws, nothing is
|
|
65
|
+
* written and the lock is released.
|
|
66
|
+
*/
|
|
67
|
+
update(fn: (prev: DailySpend | null) => DailySpend | undefined): Promise<DailySpend | null>;
|
|
68
|
+
private writeFile;
|
|
69
|
+
/** Run `fn` while holding the lock file; always releases it. */
|
|
70
|
+
private withLock;
|
|
71
|
+
private acquireLock;
|
|
72
|
+
/** Remove the lock if its holder stopped refreshing it (crashed process). */
|
|
73
|
+
private breakIfStale;
|
|
74
|
+
private releaseLock;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { FileSpendStore, type FileSpendStoreOptions };
|