@openzeppelin/relayer-plugin-channels 0.3.1 → 0.5.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 +189 -3
- package/dist/client/channels-client.d.ts +50 -1
- package/dist/client/channels-client.d.ts.map +1 -1
- package/dist/client/channels-client.js +90 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/types.d.ts +64 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/plugin/config.d.ts +5 -13
- package/dist/plugin/config.d.ts.map +1 -1
- package/dist/plugin/config.js +46 -45
- package/dist/plugin/constants.d.ts +3 -3
- package/dist/plugin/constants.d.ts.map +1 -1
- package/dist/plugin/constants.js +4 -3
- package/dist/plugin/fee-tracking.d.ts +81 -0
- package/dist/plugin/fee-tracking.d.ts.map +1 -0
- package/dist/plugin/fee-tracking.js +146 -0
- package/dist/plugin/fee.d.ts +2 -3
- package/dist/plugin/fee.d.ts.map +1 -1
- package/dist/plugin/fee.js +6 -14
- package/dist/plugin/handler.d.ts.map +1 -1
- package/dist/plugin/handler.js +36 -10
- package/dist/plugin/management.d.ts +5 -1
- package/dist/plugin/management.d.ts.map +1 -1
- package/dist/plugin/management.js +113 -18
- package/dist/plugin/pool.d.ts +1 -1
- package/dist/plugin/pool.d.ts.map +1 -1
- package/dist/plugin/pool.js +4 -6
- package/dist/plugin/simulation.d.ts +3 -2
- package/dist/plugin/simulation.d.ts.map +1 -1
- package/dist/plugin/simulation.js +49 -7
- package/dist/plugin/submit.d.ts +2 -1
- package/dist/plugin/submit.d.ts.map +1 -1
- package/dist/plugin/submit.js +10 -2
- package/package.json +13 -13
package/dist/plugin/config.js
CHANGED
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.loadConfig = loadConfig;
|
|
9
9
|
exports.getNetworkPassphrase = getNetworkPassphrase;
|
|
10
|
-
exports.getLockTtlSeconds = getLockTtlSeconds;
|
|
11
|
-
exports.getMaxFee = getMaxFee;
|
|
12
|
-
exports.getAdminSecret = getAdminSecret;
|
|
13
10
|
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
14
11
|
const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
|
|
15
12
|
const constants_1 = require("./constants");
|
|
@@ -24,6 +21,46 @@ function requireEnv(name) {
|
|
|
24
21
|
}
|
|
25
22
|
return v.trim();
|
|
26
23
|
}
|
|
24
|
+
function parseOptionalString(name) {
|
|
25
|
+
const v = process.env[name];
|
|
26
|
+
if (!v)
|
|
27
|
+
return undefined;
|
|
28
|
+
const t = v.trim();
|
|
29
|
+
return t.length ? t : undefined;
|
|
30
|
+
}
|
|
31
|
+
function parseLockTtl() {
|
|
32
|
+
const raw = process.env.LOCK_TTL_SECONDS;
|
|
33
|
+
if (!raw)
|
|
34
|
+
return constants_1.CONFIG.DEFAULT_LOCK_TTL_SECONDS;
|
|
35
|
+
const n = Number(raw);
|
|
36
|
+
if (!Number.isFinite(n) ||
|
|
37
|
+
n < constants_1.CONFIG.MIN_LOCK_TTL_SECONDS ||
|
|
38
|
+
n > constants_1.CONFIG.MAX_LOCK_TTL_SECONDS) {
|
|
39
|
+
return constants_1.CONFIG.DEFAULT_LOCK_TTL_SECONDS;
|
|
40
|
+
}
|
|
41
|
+
return Math.floor(n);
|
|
42
|
+
}
|
|
43
|
+
function parseFeeLimit() {
|
|
44
|
+
const raw = process.env.FEE_LIMIT;
|
|
45
|
+
if (!raw)
|
|
46
|
+
return undefined;
|
|
47
|
+
const n = Number(raw);
|
|
48
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : undefined;
|
|
49
|
+
}
|
|
50
|
+
function parseFeeResetPeriod() {
|
|
51
|
+
const raw = process.env.FEE_RESET_PERIOD_SECONDS;
|
|
52
|
+
if (!raw)
|
|
53
|
+
return undefined;
|
|
54
|
+
const n = Number(raw);
|
|
55
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) * 1000 : undefined;
|
|
56
|
+
}
|
|
57
|
+
function parseApiKeyHeader() {
|
|
58
|
+
const raw = process.env.API_KEY_HEADER;
|
|
59
|
+
if (!raw)
|
|
60
|
+
return "x-api-key";
|
|
61
|
+
const trimmed = raw.trim().toLowerCase();
|
|
62
|
+
return trimmed.length > 0 ? trimmed : "x-api-key";
|
|
63
|
+
}
|
|
27
64
|
/**
|
|
28
65
|
* Load configuration from environment variables
|
|
29
66
|
*/
|
|
@@ -35,12 +72,14 @@ function loadConfig() {
|
|
|
35
72
|
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
36
73
|
});
|
|
37
74
|
}
|
|
38
|
-
const fundRelayerId = requireEnv("FUND_RELAYER_ID");
|
|
39
|
-
const rpcUrl = requireEnv("SOROBAN_RPC_URL");
|
|
40
75
|
return {
|
|
41
|
-
fundRelayerId,
|
|
76
|
+
fundRelayerId: requireEnv("FUND_RELAYER_ID"),
|
|
42
77
|
network: networkRaw,
|
|
43
|
-
|
|
78
|
+
lockTtlSeconds: parseLockTtl(),
|
|
79
|
+
adminSecret: parseOptionalString("PLUGIN_ADMIN_SECRET"),
|
|
80
|
+
feeLimit: parseFeeLimit(),
|
|
81
|
+
feeResetPeriodMs: parseFeeResetPeriod(),
|
|
82
|
+
apiKeyHeader: parseApiKeyHeader(),
|
|
44
83
|
};
|
|
45
84
|
}
|
|
46
85
|
/**
|
|
@@ -49,41 +88,3 @@ function loadConfig() {
|
|
|
49
88
|
function getNetworkPassphrase(network) {
|
|
50
89
|
return network === "mainnet" ? stellar_sdk_1.Networks.PUBLIC : stellar_sdk_1.Networks.TESTNET;
|
|
51
90
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Get the per-channel lock TTL in seconds (default 30)
|
|
54
|
-
*/
|
|
55
|
-
function getLockTtlSeconds() {
|
|
56
|
-
const raw = process.env.LOCK_TTL_SECONDS;
|
|
57
|
-
if (!raw)
|
|
58
|
-
return constants_1.CONFIG.DEFAULT_LOCK_TTL_SECONDS;
|
|
59
|
-
const n = Number(raw);
|
|
60
|
-
if (!Number.isFinite(n) ||
|
|
61
|
-
n < constants_1.CONFIG.MIN_LOCK_TTL_SECONDS ||
|
|
62
|
-
n > constants_1.CONFIG.MAX_LOCK_TTL_SECONDS) {
|
|
63
|
-
return constants_1.CONFIG.DEFAULT_LOCK_TTL_SECONDS;
|
|
64
|
-
}
|
|
65
|
-
return Math.floor(n);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Get the max fee for fee bump transactions
|
|
69
|
-
*/
|
|
70
|
-
function getMaxFee() {
|
|
71
|
-
const raw = process.env.MAX_FEE;
|
|
72
|
-
if (!raw)
|
|
73
|
-
return constants_1.CONFIG.DEFAULT_MAX_FEE;
|
|
74
|
-
const n = Number(raw);
|
|
75
|
-
if (!Number.isFinite(n) || n <= 0) {
|
|
76
|
-
return constants_1.CONFIG.DEFAULT_MAX_FEE;
|
|
77
|
-
}
|
|
78
|
-
return Math.floor(n);
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Get the admin secret for management API
|
|
82
|
-
*/
|
|
83
|
-
function getAdminSecret() {
|
|
84
|
-
const v = process.env.PLUGIN_ADMIN_SECRET;
|
|
85
|
-
if (!v)
|
|
86
|
-
return undefined;
|
|
87
|
-
const t = v.trim();
|
|
88
|
-
return t.length ? t : undefined;
|
|
89
|
-
}
|
|
@@ -8,6 +8,7 @@ export declare const HTTP_STATUS: {
|
|
|
8
8
|
readonly UNAUTHORIZED: 401;
|
|
9
9
|
readonly FORBIDDEN: 403;
|
|
10
10
|
readonly CONFLICT: 409;
|
|
11
|
+
readonly TOO_MANY_REQUESTS: 429;
|
|
11
12
|
readonly INTERNAL_SERVER_ERROR: 500;
|
|
12
13
|
readonly BAD_GATEWAY: 502;
|
|
13
14
|
readonly SERVICE_UNAVAILABLE: 503;
|
|
@@ -15,9 +16,8 @@ export declare const HTTP_STATUS: {
|
|
|
15
16
|
};
|
|
16
17
|
export declare const CONFIG: {
|
|
17
18
|
readonly DEFAULT_LOCK_TTL_SECONDS: 30;
|
|
18
|
-
readonly MIN_LOCK_TTL_SECONDS:
|
|
19
|
+
readonly MIN_LOCK_TTL_SECONDS: 3;
|
|
19
20
|
readonly MAX_LOCK_TTL_SECONDS: 30;
|
|
20
|
-
readonly DEFAULT_MAX_FEE: 1000000;
|
|
21
21
|
};
|
|
22
22
|
export declare const POOL: {
|
|
23
23
|
readonly MUTEX_TTL_SECONDS: 1;
|
|
@@ -41,6 +41,6 @@ export declare const POLLING: {
|
|
|
41
41
|
export declare const FEE: {
|
|
42
42
|
readonly MIN_BASE_FEE: 205;
|
|
43
43
|
readonly MAX_BASE_FEE: 605;
|
|
44
|
-
readonly
|
|
44
|
+
readonly NON_SOROBAN_FEE: 100000;
|
|
45
45
|
};
|
|
46
46
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/plugin/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,WAAW
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/plugin/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,WAAW;;;;;;;;;;CAUd,CAAC;AAGX,eAAO,MAAM,MAAM;;;;CAIT,CAAC;AAGX,eAAO,MAAM,IAAI;;;;;CAOP,CAAC;AAGX,eAAO,MAAM,IAAI;;CAEP,CAAC;AAGX,eAAO,MAAM,UAAU;;;;;CAKb,CAAC;AAGX,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAEX,eAAO,MAAM,GAAG;;;;CAKN,CAAC"}
|
package/dist/plugin/constants.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.HTTP_STATUS = {
|
|
|
12
12
|
UNAUTHORIZED: 401,
|
|
13
13
|
FORBIDDEN: 403,
|
|
14
14
|
CONFLICT: 409,
|
|
15
|
+
TOO_MANY_REQUESTS: 429,
|
|
15
16
|
INTERNAL_SERVER_ERROR: 500,
|
|
16
17
|
BAD_GATEWAY: 502,
|
|
17
18
|
SERVICE_UNAVAILABLE: 503,
|
|
@@ -20,9 +21,8 @@ exports.HTTP_STATUS = {
|
|
|
20
21
|
// Configuration Constants
|
|
21
22
|
exports.CONFIG = {
|
|
22
23
|
DEFAULT_LOCK_TTL_SECONDS: 30,
|
|
23
|
-
MIN_LOCK_TTL_SECONDS:
|
|
24
|
+
MIN_LOCK_TTL_SECONDS: 3,
|
|
24
25
|
MAX_LOCK_TTL_SECONDS: 30,
|
|
25
|
-
DEFAULT_MAX_FEE: 1000000,
|
|
26
26
|
};
|
|
27
27
|
// Pool Constants
|
|
28
28
|
exports.POOL = {
|
|
@@ -52,5 +52,6 @@ exports.POLLING = {
|
|
|
52
52
|
exports.FEE = {
|
|
53
53
|
MIN_BASE_FEE: 205,
|
|
54
54
|
MAX_BASE_FEE: 605,
|
|
55
|
-
|
|
55
|
+
// For non-Soroban txs: 100,000 stroops (0.01 XLM) per Stellar best practice
|
|
56
|
+
NON_SOROBAN_FEE: 100000,
|
|
56
57
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fee-tracking.ts
|
|
3
|
+
*
|
|
4
|
+
* API key fee tracking for rate limiting.
|
|
5
|
+
* Supports custom per-key limits with fallback to default.
|
|
6
|
+
* Supports periodic reset of consumption.
|
|
7
|
+
*/
|
|
8
|
+
import { PluginKVStore } from "@openzeppelin/relayer-sdk";
|
|
9
|
+
/** KV data structure for fee consumption */
|
|
10
|
+
export interface FeeData {
|
|
11
|
+
consumed: number;
|
|
12
|
+
periodStart?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Usage info returned by getUsageInfo */
|
|
15
|
+
export interface UsageInfo {
|
|
16
|
+
consumed: number;
|
|
17
|
+
limit?: number;
|
|
18
|
+
remaining?: number;
|
|
19
|
+
periodStartAt?: string;
|
|
20
|
+
periodEndsAt?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Configuration for FeeTracker */
|
|
23
|
+
export interface FeeTrackerConfig {
|
|
24
|
+
kv: PluginKVStore;
|
|
25
|
+
network: "testnet" | "mainnet";
|
|
26
|
+
apiKey: string;
|
|
27
|
+
defaultLimit?: number;
|
|
28
|
+
resetPeriodMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class FeeTracker {
|
|
31
|
+
private readonly kv;
|
|
32
|
+
private readonly network;
|
|
33
|
+
private readonly apiKey;
|
|
34
|
+
private readonly defaultLimit?;
|
|
35
|
+
private readonly resetPeriodMs?;
|
|
36
|
+
constructor(config: FeeTrackerConfig);
|
|
37
|
+
/**
|
|
38
|
+
* Check if API key can afford the given fee.
|
|
39
|
+
* Throws 429 if not. No-op if unlimited.
|
|
40
|
+
*/
|
|
41
|
+
checkBudget(fee: number): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Record fee consumption after transaction.
|
|
44
|
+
* Uses distributed lock for atomicity with retry.
|
|
45
|
+
* Errors are logged but not thrown (non-blocking).
|
|
46
|
+
*/
|
|
47
|
+
recordUsage(fee: number): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Get complete usage info for management API.
|
|
50
|
+
*/
|
|
51
|
+
getUsageInfo(): Promise<UsageInfo>;
|
|
52
|
+
/**
|
|
53
|
+
* Get custom limit for this API key.
|
|
54
|
+
*/
|
|
55
|
+
getCustomLimit(): Promise<number | undefined>;
|
|
56
|
+
/**
|
|
57
|
+
* Set custom limit for this API key.
|
|
58
|
+
*/
|
|
59
|
+
setCustomLimit(limit: number): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Delete custom limit for this API key.
|
|
62
|
+
*/
|
|
63
|
+
deleteCustomLimit(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Get effective fee state after applying period expiry.
|
|
66
|
+
* Returns { consumed: 0 } if no data or period expired.
|
|
67
|
+
*/
|
|
68
|
+
private getFeeState;
|
|
69
|
+
/**
|
|
70
|
+
* Get effective limit (custom ?? default).
|
|
71
|
+
*/
|
|
72
|
+
private getEffectiveLimit;
|
|
73
|
+
/**
|
|
74
|
+
* Check if period has expired.
|
|
75
|
+
*/
|
|
76
|
+
private isPeriodExpired;
|
|
77
|
+
private get consumedKey();
|
|
78
|
+
private get limitKey();
|
|
79
|
+
private get lockKey();
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=fee-tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fee-tracking.d.ts","sourceRoot":"","sources":["../../src/plugin/fee-tracking.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AAGvE,4CAA4C;AAC5C,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAQD,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mCAAmC;AACnC,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,aAAa,CAAC;IAClB,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAS;gBAE5B,MAAM,EAAE,gBAAgB;IAUpC;;;OAGG;IACG,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7C;;;;OAIG;IACG,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B7C;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAqBxC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAKnD;;OAEG;IACG,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAMxC;;;OAGG;YACW,WAAW;IASzB;;OAEG;YACW,iBAAiB;IAK/B;;OAEG;IACH,OAAO,CAAC,eAAe;IAKvB,OAAO,KAAK,WAAW,GAEtB;IAED,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,KAAK,OAAO,GAElB;CACF"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* fee-tracking.ts
|
|
4
|
+
*
|
|
5
|
+
* API key fee tracking for rate limiting.
|
|
6
|
+
* Supports custom per-key limits with fallback to default.
|
|
7
|
+
* Supports periodic reset of consumption.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.FeeTracker = void 0;
|
|
11
|
+
const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
|
|
12
|
+
const constants_1 = require("./constants");
|
|
13
|
+
class FeeTracker {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.kv = config.kv;
|
|
16
|
+
this.network = config.network;
|
|
17
|
+
this.apiKey = config.apiKey;
|
|
18
|
+
this.defaultLimit = config.defaultLimit;
|
|
19
|
+
this.resetPeriodMs = config.resetPeriodMs;
|
|
20
|
+
}
|
|
21
|
+
// === Public API: Transaction Flow ===
|
|
22
|
+
/**
|
|
23
|
+
* Check if API key can afford the given fee.
|
|
24
|
+
* Throws 429 if not. No-op if unlimited.
|
|
25
|
+
*/
|
|
26
|
+
async checkBudget(fee) {
|
|
27
|
+
const limit = await this.getEffectiveLimit();
|
|
28
|
+
if (limit === undefined)
|
|
29
|
+
return;
|
|
30
|
+
const { consumed } = await this.getFeeState();
|
|
31
|
+
if (consumed + fee > limit) {
|
|
32
|
+
const remaining = limit - consumed;
|
|
33
|
+
throw (0, relayer_sdk_1.pluginError)(`Transaction fee (${fee} stroops) exceeds remaining budget (${remaining} stroops). Consumed: ${consumed}/${limit} stroops.`, {
|
|
34
|
+
code: "FEE_LIMIT_EXCEEDED",
|
|
35
|
+
status: constants_1.HTTP_STATUS.TOO_MANY_REQUESTS,
|
|
36
|
+
details: { consumed, fee, remaining, limit },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Record fee consumption after transaction.
|
|
42
|
+
* Uses distributed lock for atomicity with retry.
|
|
43
|
+
* Errors are logged but not thrown (non-blocking).
|
|
44
|
+
*/
|
|
45
|
+
async recordUsage(fee) {
|
|
46
|
+
const maxRetries = 3;
|
|
47
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
48
|
+
try {
|
|
49
|
+
const result = await this.kv.withLock(this.lockKey, async () => {
|
|
50
|
+
const state = await this.getFeeState();
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
await this.kv.set(this.consumedKey, {
|
|
53
|
+
consumed: state.consumed + fee,
|
|
54
|
+
periodStart: state.periodStart ?? now,
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
}, { ttlSec: 5, onBusy: "skip" });
|
|
58
|
+
if (result !== null)
|
|
59
|
+
return;
|
|
60
|
+
// Lock busy, retry with jitter
|
|
61
|
+
await new Promise((r) => setTimeout(r, 20 + Math.random() * 50));
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.warn(`[channels] Failed to record fee: ${err}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
console.warn(`[channels] Failed to record fee after ${maxRetries} retries (lock contention)`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get complete usage info for management API.
|
|
72
|
+
*/
|
|
73
|
+
async getUsageInfo() {
|
|
74
|
+
const [state, limit] = await Promise.all([
|
|
75
|
+
this.getFeeState(),
|
|
76
|
+
this.getEffectiveLimit(),
|
|
77
|
+
]);
|
|
78
|
+
return {
|
|
79
|
+
consumed: state.consumed,
|
|
80
|
+
limit,
|
|
81
|
+
remaining: limit !== undefined ? Math.max(0, limit - state.consumed) : undefined,
|
|
82
|
+
periodStartAt: state.periodStart
|
|
83
|
+
? new Date(state.periodStart).toISOString()
|
|
84
|
+
: undefined,
|
|
85
|
+
periodEndsAt: state.periodStart && this.resetPeriodMs
|
|
86
|
+
? new Date(state.periodStart + this.resetPeriodMs).toISOString()
|
|
87
|
+
: undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get custom limit for this API key.
|
|
92
|
+
*/
|
|
93
|
+
async getCustomLimit() {
|
|
94
|
+
const data = await this.kv.get(this.limitKey);
|
|
95
|
+
return data?.limit;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Set custom limit for this API key.
|
|
99
|
+
*/
|
|
100
|
+
async setCustomLimit(limit) {
|
|
101
|
+
await this.kv.set(this.limitKey, { limit });
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Delete custom limit for this API key.
|
|
105
|
+
*/
|
|
106
|
+
async deleteCustomLimit() {
|
|
107
|
+
await this.kv.del(this.limitKey);
|
|
108
|
+
}
|
|
109
|
+
// === Private ===
|
|
110
|
+
/**
|
|
111
|
+
* Get effective fee state after applying period expiry.
|
|
112
|
+
* Returns { consumed: 0 } if no data or period expired.
|
|
113
|
+
*/
|
|
114
|
+
async getFeeState() {
|
|
115
|
+
const data = await this.kv.get(this.consumedKey);
|
|
116
|
+
if (!data || this.isPeriodExpired(data.periodStart)) {
|
|
117
|
+
return { consumed: 0 };
|
|
118
|
+
}
|
|
119
|
+
return data;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get effective limit (custom ?? default).
|
|
123
|
+
*/
|
|
124
|
+
async getEffectiveLimit() {
|
|
125
|
+
const custom = await this.getCustomLimit();
|
|
126
|
+
return custom ?? this.defaultLimit;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if period has expired.
|
|
130
|
+
*/
|
|
131
|
+
isPeriodExpired(periodStart) {
|
|
132
|
+
if (!this.resetPeriodMs || !periodStart)
|
|
133
|
+
return false;
|
|
134
|
+
return Date.now() - periodStart >= this.resetPeriodMs;
|
|
135
|
+
}
|
|
136
|
+
get consumedKey() {
|
|
137
|
+
return `${this.network}:api-key-fees:${this.apiKey}`;
|
|
138
|
+
}
|
|
139
|
+
get limitKey() {
|
|
140
|
+
return `${this.network}:api-key-limit:${this.apiKey}`;
|
|
141
|
+
}
|
|
142
|
+
get lockKey() {
|
|
143
|
+
return `${this.network}:fee-lock:${this.apiKey}`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.FeeTracker = FeeTracker;
|
package/dist/plugin/fee.d.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* fee.ts
|
|
3
3
|
*
|
|
4
4
|
* Dynamic fee calculation for fee bump submissions.
|
|
5
|
-
* - For Soroban transactions
|
|
6
|
-
* - For non-Soroban
|
|
7
|
-
* - Optionally clamp to env MAX_FEE via config helper
|
|
5
|
+
* - For Soroban transactions: use resourceFee + random inclusion fee
|
|
6
|
+
* - For non-Soroban: use NON_SOROBAN_FEE
|
|
8
7
|
*/
|
|
9
8
|
import { Transaction } from "@stellar/stellar-sdk";
|
|
10
9
|
export declare function calculateMaxFee(transaction: Transaction): number;
|
package/dist/plugin/fee.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fee.d.ts","sourceRoot":"","sources":["../../src/plugin/fee.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fee.d.ts","sourceRoot":"","sources":["../../src/plugin/fee.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAGxD,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CAsBhE"}
|
package/dist/plugin/fee.js
CHANGED
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
* fee.ts
|
|
4
4
|
*
|
|
5
5
|
* Dynamic fee calculation for fee bump submissions.
|
|
6
|
-
* - For Soroban transactions
|
|
7
|
-
* - For non-Soroban
|
|
8
|
-
* - Optionally clamp to env MAX_FEE via config helper
|
|
6
|
+
* - For Soroban transactions: use resourceFee + random inclusion fee
|
|
7
|
+
* - For non-Soroban: use NON_SOROBAN_FEE
|
|
9
8
|
*/
|
|
10
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
10
|
exports.calculateMaxFee = calculateMaxFee;
|
|
12
11
|
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
13
12
|
const constants_1 = require("./constants");
|
|
14
|
-
const config_1 = require("./config");
|
|
15
13
|
function calculateMaxFee(transaction) {
|
|
16
14
|
const envelope = transaction.toEnvelope();
|
|
17
15
|
let resourceFee = 0n;
|
|
@@ -22,17 +20,11 @@ function calculateMaxFee(transaction) {
|
|
|
22
20
|
}
|
|
23
21
|
}
|
|
24
22
|
const baseInclusion = getRandomInt(constants_1.FEE.MIN_BASE_FEE, constants_1.FEE.MAX_BASE_FEE);
|
|
25
|
-
|
|
23
|
+
const fee = resourceFee > 0n
|
|
26
24
|
? resourceFee + BigInt(baseInclusion)
|
|
27
|
-
: BigInt(constants_1.FEE.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (typeof cap === "number" && Number.isFinite(cap) && cap > 0) {
|
|
31
|
-
if (dynamic > BigInt(cap))
|
|
32
|
-
dynamic = BigInt(cap);
|
|
33
|
-
}
|
|
34
|
-
console.debug(`[channels] Calculated max_fee: ${Number(dynamic)} stroops (resourceFee: ${resourceFee}, baseInclusion: ${baseInclusion})`);
|
|
35
|
-
return Number(dynamic);
|
|
25
|
+
: BigInt(constants_1.FEE.NON_SOROBAN_FEE + baseInclusion);
|
|
26
|
+
console.debug(`[channels] Calculated max_fee: ${Number(fee)} stroops (resourceFee: ${resourceFee}, baseInclusion: ${baseInclusion})`);
|
|
27
|
+
return Number(fee);
|
|
36
28
|
}
|
|
37
29
|
function getRandomInt(min, max) {
|
|
38
30
|
const mn = Math.ceil(min);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/plugin/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/plugin/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AAqNvE;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAGlE"}
|
package/dist/plugin/handler.js
CHANGED
|
@@ -18,13 +18,19 @@ const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
|
18
18
|
const simulation_1 = require("./simulation");
|
|
19
19
|
const fee_1 = require("./fee");
|
|
20
20
|
const tx_1 = require("./tx");
|
|
21
|
-
|
|
21
|
+
const fee_tracking_1 = require("./fee-tracking");
|
|
22
|
+
function getApiKey(headers, headerName) {
|
|
23
|
+
const values = headers[headerName];
|
|
24
|
+
return values?.[0]?.trim() || undefined;
|
|
25
|
+
}
|
|
26
|
+
async function handleXdrSubmit(xdrStr, fundRelayer, network, networkPassphrase, api, tracker) {
|
|
22
27
|
const tx = new stellar_sdk_1.Transaction(xdrStr, networkPassphrase);
|
|
23
28
|
const validated = (0, tx_1.validateExistingTransactionForSubmitOnly)(tx);
|
|
24
29
|
const maxFee = (0, fee_1.calculateMaxFee)(validated);
|
|
25
|
-
|
|
30
|
+
await tracker?.checkBudget(maxFee);
|
|
31
|
+
return (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, validated.toXDR(), network, maxFee, api, tracker);
|
|
26
32
|
}
|
|
27
|
-
async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddress, network, networkPassphrase,
|
|
33
|
+
async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddress, network, networkPassphrase, tracker) {
|
|
28
34
|
let poolLock;
|
|
29
35
|
try {
|
|
30
36
|
poolLock = await pool.acquire();
|
|
@@ -49,10 +55,11 @@ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddr
|
|
|
49
55
|
},
|
|
50
56
|
});
|
|
51
57
|
}
|
|
52
|
-
const built = await (0, simulation_1.simulateAndBuildWithChannel)(func, auth, { address: channelInfo.address, sequence: channelStatus.sequence_number }, fundAddress,
|
|
58
|
+
const built = await (0, simulation_1.simulateAndBuildWithChannel)(func, auth, { address: channelInfo.address, sequence: channelStatus.sequence_number }, fundAddress, fundRelayer, networkPassphrase);
|
|
53
59
|
const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, fundRelayer, channelInfo.address, fundAddress, networkPassphrase);
|
|
54
60
|
const maxFee = (0, fee_1.calculateMaxFee)(signedTx);
|
|
55
|
-
|
|
61
|
+
await tracker?.checkBudget(maxFee);
|
|
62
|
+
return await (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, signedTx.toXDR(), network, maxFee, api, tracker);
|
|
56
63
|
}
|
|
57
64
|
finally {
|
|
58
65
|
if (poolLock) {
|
|
@@ -61,16 +68,35 @@ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddr
|
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
async function channelAccounts(context) {
|
|
64
|
-
const { api, kv, params } = context;
|
|
71
|
+
const { api, kv, params, headers } = context;
|
|
65
72
|
// Management branch: handle and return immediately
|
|
66
73
|
if ((0, management_1.isManagementRequest)(params)) {
|
|
67
74
|
return await (0, management_1.handleManagement)(context);
|
|
68
75
|
}
|
|
69
76
|
// Load config and initialize per-request dependencies
|
|
70
77
|
const config = (0, config_1.loadConfig)();
|
|
71
|
-
const pool = new pool_1.ChannelPool(config.network, kv);
|
|
78
|
+
const pool = new pool_1.ChannelPool(config.network, kv, config.lockTtlSeconds);
|
|
72
79
|
const networkPassphrase = (0, config_1.getNetworkPassphrase)(config.network);
|
|
73
|
-
|
|
80
|
+
// Fee tracking setup
|
|
81
|
+
let tracker;
|
|
82
|
+
const apiKey = getApiKey(headers, config.apiKeyHeader);
|
|
83
|
+
// If default limit is set, require API key
|
|
84
|
+
if (config.feeLimit !== undefined && !apiKey) {
|
|
85
|
+
throw (0, relayer_sdk_1.pluginError)("API key required", {
|
|
86
|
+
code: "API_KEY_REQUIRED",
|
|
87
|
+
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// Create tracker if API key is present (for tracking and custom limits)
|
|
91
|
+
if (apiKey) {
|
|
92
|
+
tracker = new fee_tracking_1.FeeTracker({
|
|
93
|
+
kv,
|
|
94
|
+
network: config.network,
|
|
95
|
+
apiKey,
|
|
96
|
+
defaultLimit: config.feeLimit,
|
|
97
|
+
resetPeriodMs: config.feeResetPeriodMs,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
74
100
|
try {
|
|
75
101
|
// 1. Validate and parse request (xdr OR func+auth)
|
|
76
102
|
const request = (0, validation_1.validateAndParseRequest)(params);
|
|
@@ -99,10 +125,10 @@ async function channelAccounts(context) {
|
|
|
99
125
|
// 3. Branch by request type
|
|
100
126
|
if (request.type === "xdr") {
|
|
101
127
|
console.log(`[channels] Flow: XDR submit-only`);
|
|
102
|
-
return await handleXdrSubmit(request.xdr, fundRelayer, config.network, networkPassphrase, api);
|
|
128
|
+
return await handleXdrSubmit(request.xdr, fundRelayer, config.network, networkPassphrase, api, tracker);
|
|
103
129
|
}
|
|
104
130
|
console.log(`[channels] Flow: func+auth with channel account`);
|
|
105
|
-
return await handleFuncAuthSubmit(request.func, request.auth, api, pool, fundRelayer, fundInfo.address, config.network, networkPassphrase,
|
|
131
|
+
return await handleFuncAuthSubmit(request.func, request.auth, api, pool, fundRelayer, fundInfo.address, config.network, networkPassphrase, tracker);
|
|
106
132
|
}
|
|
107
133
|
finally {
|
|
108
134
|
// Nothing to cleanup here; func-auth path releases locks internally
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* management.ts
|
|
3
3
|
*
|
|
4
|
-
* Payload-based management API for channel relayerIds.
|
|
4
|
+
* Payload-based management API for channel relayerIds and fee limits.
|
|
5
5
|
* - listChannelAccounts: returns relayerIds from KV
|
|
6
6
|
* - setChannelAccounts: replaces relayerIds array in KV (checks lock conflicts)
|
|
7
|
+
* - getFeeUsage: returns fee consumption for an API key
|
|
8
|
+
* - getFeeLimit: returns custom limit for an API key (if set)
|
|
9
|
+
* - setFeeLimit: sets custom limit for an API key
|
|
10
|
+
* - deleteFeeLimit: removes custom limit for an API key
|
|
7
11
|
*/
|
|
8
12
|
import type { PluginContext } from "@openzeppelin/relayer-sdk";
|
|
9
13
|
export declare function isManagementRequest(params: any): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"management.d.ts","sourceRoot":"","sources":["../../src/plugin/management.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"management.d.ts","sourceRoot":"","sources":["../../src/plugin/management.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,2BAA2B,CAAC;AAM9E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAOxD;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CA8C3E"}
|