@openzeppelin/relayer-plugin-channels 0.4.0 → 0.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/plugin/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,WAAW;;;;;;;;;CASd,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"}
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;;CAGN,CAAC"}
@@ -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,7 +21,7 @@ exports.HTTP_STATUS = {
20
21
  // Configuration Constants
21
22
  exports.CONFIG = {
22
23
  DEFAULT_LOCK_TTL_SECONDS: 30,
23
- MIN_LOCK_TTL_SECONDS: 10,
24
+ MIN_LOCK_TTL_SECONDS: 3,
24
25
  MAX_LOCK_TTL_SECONDS: 30,
25
26
  };
26
27
  // Pool Constants
@@ -49,8 +50,6 @@ exports.POLLING = {
49
50
  TIMEOUT_MS: 25000,
50
51
  };
51
52
  exports.FEE = {
52
- MIN_BASE_FEE: 205,
53
- MAX_BASE_FEE: 605,
54
53
  // For non-Soroban txs: 100,000 stroops (0.01 XLM) per Stellar best practice
55
54
  NON_SOROBAN_FEE: 100000,
56
55
  };
@@ -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;
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * fee.ts
3
3
  *
4
- * Dynamic fee calculation for fee bump submissions.
5
- * - For Soroban transactions: use resourceFee + random inclusion fee
6
- * - For non-Soroban: use NON_SOROBAN_FEE
4
+ * Static fee calculation for fee bump submissions matching launchtube.
5
+ * - For Soroban transactions: use resourceFee + inclusion fee (BASE_FEE * 2 + 3)
6
+ * - For non-Soroban: use NON_SOROBAN_FEE + inclusion fee
7
+ * - KALE contract gets reduced fee (BASE_FEE * 2 + 1)
7
8
  */
8
9
  import { Transaction } from "@stellar/stellar-sdk";
10
+ export declare const KALE_CONTRACT = "CDL74RF5BLYR2YBLCCI7F5FB6TPSCLKEJUBSD2RSVWZ4YHF3VMFAIGWA";
11
+ export declare const INCLUSION_FEE_DEFAULT: number;
12
+ export declare const INCLUSION_FEE_KALE: number;
9
13
  export declare function calculateMaxFee(transaction: Transaction): number;
10
14
  //# sourceMappingURL=fee.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"fee.d.ts","sourceRoot":"","sources":["../../src/plugin/fee.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,WAAW,EAIZ,MAAM,sBAAsB,CAAC;AAG9B,eAAO,MAAM,aAAa,6DACkC,CAAC;AAC7D,eAAO,MAAM,qBAAqB,QAA2B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA2B,CAAC;AA8B3D,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CAsBhE"}
@@ -2,14 +2,41 @@
2
2
  /**
3
3
  * fee.ts
4
4
  *
5
- * Dynamic fee calculation for fee bump submissions.
6
- * - For Soroban transactions: use resourceFee + random inclusion fee
7
- * - For non-Soroban: use NON_SOROBAN_FEE
5
+ * Static fee calculation for fee bump submissions matching launchtube.
6
+ * - For Soroban transactions: use resourceFee + inclusion fee (BASE_FEE * 2 + 3)
7
+ * - For non-Soroban: use NON_SOROBAN_FEE + inclusion fee
8
+ * - KALE contract gets reduced fee (BASE_FEE * 2 + 1)
8
9
  */
9
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.INCLUSION_FEE_KALE = exports.INCLUSION_FEE_DEFAULT = exports.KALE_CONTRACT = void 0;
10
12
  exports.calculateMaxFee = calculateMaxFee;
11
13
  const stellar_sdk_1 = require("@stellar/stellar-sdk");
12
14
  const constants_1 = require("./constants");
15
+ exports.KALE_CONTRACT = "CDL74RF5BLYR2YBLCCI7F5FB6TPSCLKEJUBSD2RSVWZ4YHF3VMFAIGWA";
16
+ exports.INCLUSION_FEE_DEFAULT = Number(stellar_sdk_1.BASE_FEE) * 2 + 3;
17
+ exports.INCLUSION_FEE_KALE = Number(stellar_sdk_1.BASE_FEE) * 2 + 1;
18
+ function getInclusionFee(transaction) {
19
+ try {
20
+ if (transaction.operations.length !== 1)
21
+ return exports.INCLUSION_FEE_DEFAULT;
22
+ const op = transaction.operations[0];
23
+ if (op.type !== "invokeHostFunction")
24
+ return exports.INCLUSION_FEE_DEFAULT;
25
+ const invokeOp = op;
26
+ if (invokeOp.func.switch() !==
27
+ stellar_sdk_1.xdr.HostFunctionType.hostFunctionTypeInvokeContract()) {
28
+ return exports.INCLUSION_FEE_DEFAULT;
29
+ }
30
+ const invokeContract = invokeOp.func.invokeContract();
31
+ const contract = stellar_sdk_1.StrKey.encodeContract(invokeContract.contractAddress().contractId());
32
+ return contract === exports.KALE_CONTRACT
33
+ ? exports.INCLUSION_FEE_KALE
34
+ : exports.INCLUSION_FEE_DEFAULT;
35
+ }
36
+ catch {
37
+ return exports.INCLUSION_FEE_DEFAULT;
38
+ }
39
+ }
13
40
  function calculateMaxFee(transaction) {
14
41
  const envelope = transaction.toEnvelope();
15
42
  let resourceFee = 0n;
@@ -19,15 +46,10 @@ function calculateMaxFee(transaction) {
19
46
  resourceFee = sorobanData.resourceFee().toBigInt();
20
47
  }
21
48
  }
22
- const baseInclusion = getRandomInt(constants_1.FEE.MIN_BASE_FEE, constants_1.FEE.MAX_BASE_FEE);
49
+ const inclusionFee = getInclusionFee(transaction);
23
50
  const fee = resourceFee > 0n
24
- ? resourceFee + BigInt(baseInclusion)
25
- : BigInt(constants_1.FEE.NON_SOROBAN_FEE + baseInclusion);
26
- console.debug(`[channels] Calculated max_fee: ${Number(fee)} stroops (resourceFee: ${resourceFee}, baseInclusion: ${baseInclusion})`);
51
+ ? resourceFee + BigInt(inclusionFee)
52
+ : BigInt(constants_1.FEE.NON_SOROBAN_FEE + inclusionFee);
53
+ console.debug(`[channels] Calculated max_fee: ${Number(fee)} stroops (resourceFee: ${resourceFee}, inclusionFee: ${inclusionFee})`);
27
54
  return Number(fee);
28
55
  }
29
- function getRandomInt(min, max) {
30
- const mn = Math.ceil(min);
31
- const mx = Math.floor(max);
32
- return Math.floor(Math.random() * (mx - mn + 1)) + mn;
33
- }
@@ -5,6 +5,15 @@
5
5
  * Orchestrates the transaction processing pipeline using channel accounts with fee bumping.
6
6
  */
7
7
  import { PluginContext } from "@openzeppelin/relayer-sdk";
8
+ import { Transaction, xdr } from "@stellar/stellar-sdk";
9
+ /**
10
+ * Extracts func and auth from an unsigned Soroban transaction.
11
+ * Returns null if the transaction is not a single invokeHostFunction operation.
12
+ */
13
+ export declare function extractFuncAuthFromUnsignedXdr(tx: Transaction): {
14
+ func: xdr.HostFunction;
15
+ auth: xdr.SorobanAuthorizationEntry[];
16
+ } | null;
8
17
  /**
9
18
  * Main plugin handler exported for OpenZeppelin Relayer
10
19
  */
@@ -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;AA6KvE;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAGlE"}
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;AASvE,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAcxD;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,WAAW,GACd;IAAE,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,CAAA;CAAE,GAAG,IAAI,CAkB1E;AAiOD;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAElE"}
@@ -6,6 +6,7 @@
6
6
  * Orchestrates the transaction processing pipeline using channel accounts with fee bumping.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.extractFuncAuthFromUnsignedXdr = extractFuncAuthFromUnsignedXdr;
9
10
  exports.handler = handler;
10
11
  const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
11
12
  const pool_1 = require("./pool");
@@ -18,13 +19,55 @@ const stellar_sdk_1 = require("@stellar/stellar-sdk");
18
19
  const simulation_1 = require("./simulation");
19
20
  const fee_1 = require("./fee");
20
21
  const tx_1 = require("./tx");
21
- async function handleXdrSubmit(xdrStr, fundRelayer, network, networkPassphrase, api) {
22
+ const fee_tracking_1 = require("./fee-tracking");
23
+ function getApiKey(headers, headerName) {
24
+ const values = headers[headerName];
25
+ return values?.[0]?.trim() || undefined;
26
+ }
27
+ /**
28
+ * Extracts func and auth from an unsigned Soroban transaction.
29
+ * Returns null if the transaction is not a single invokeHostFunction operation.
30
+ */
31
+ function extractFuncAuthFromUnsignedXdr(tx) {
32
+ const ops = tx.operations;
33
+ if (ops.length !== 1) {
34
+ return null;
35
+ }
36
+ const envelope = tx.toEnvelope();
37
+ const rawOp = envelope.v1().tx().operations()[0].body();
38
+ if (rawOp.switch() !== stellar_sdk_1.xdr.OperationType.invokeHostFunction()) {
39
+ return null;
40
+ }
41
+ const invokeHostFn = rawOp.invokeHostFunctionOp();
42
+ return {
43
+ func: invokeHostFn.hostFunction(),
44
+ auth: invokeHostFn.auth(),
45
+ };
46
+ }
47
+ async function handleXdrSubmit(xdrStr, fundRelayer, fundAddress, network, networkPassphrase, api, pool, tracker) {
22
48
  const tx = new stellar_sdk_1.Transaction(xdrStr, networkPassphrase);
49
+ // Unsigned XDR: extract func+auth and route through channel path
50
+ if (tx.signatures.length === 0) {
51
+ const extracted = extractFuncAuthFromUnsignedXdr(tx);
52
+ if (!extracted) {
53
+ throw (0, relayer_sdk_1.pluginError)("Unsigned XDR must contain exactly one invokeHostFunction operation", {
54
+ code: "INVALID_UNSIGNED_XDR",
55
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
56
+ details: {
57
+ operationCount: tx.operations.length,
58
+ operationType: tx.operations[0]?.type,
59
+ },
60
+ });
61
+ }
62
+ console.log(`[channels] Unsigned XDR detected, extracting func+auth and routing through channel path`);
63
+ return handleFuncAuthSubmit(extracted.func, extracted.auth, api, pool, fundRelayer, fundAddress, network, networkPassphrase, tracker);
64
+ }
23
65
  const validated = (0, tx_1.validateExistingTransactionForSubmitOnly)(tx);
24
66
  const maxFee = (0, fee_1.calculateMaxFee)(validated);
25
- return (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, validated.toXDR(), network, maxFee, api);
67
+ await tracker?.checkBudget(maxFee);
68
+ return (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, validated.toXDR(), network, maxFee, api, tracker);
26
69
  }
27
- async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddress, network, networkPassphrase) {
70
+ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddress, network, networkPassphrase, tracker) {
28
71
  let poolLock;
29
72
  try {
30
73
  poolLock = await pool.acquire();
@@ -52,7 +95,8 @@ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddr
52
95
  const built = await (0, simulation_1.simulateAndBuildWithChannel)(func, auth, { address: channelInfo.address, sequence: channelStatus.sequence_number }, fundAddress, fundRelayer, networkPassphrase);
53
96
  const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, fundRelayer, channelInfo.address, fundAddress, networkPassphrase);
54
97
  const maxFee = (0, fee_1.calculateMaxFee)(signedTx);
55
- return await (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, signedTx.toXDR(), network, maxFee, api);
98
+ await tracker?.checkBudget(maxFee);
99
+ return await (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, signedTx.toXDR(), network, maxFee, api, tracker);
56
100
  }
57
101
  finally {
58
102
  if (poolLock) {
@@ -61,56 +105,69 @@ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddr
61
105
  }
62
106
  }
63
107
  async function channelAccounts(context) {
64
- const { api, kv, params } = context;
108
+ const { api, kv, params, headers } = context;
65
109
  // Management branch: handle and return immediately
66
110
  if ((0, management_1.isManagementRequest)(params)) {
67
111
  return await (0, management_1.handleManagement)(context);
68
112
  }
69
113
  // Load config and initialize per-request dependencies
70
114
  const config = (0, config_1.loadConfig)();
71
- const pool = new pool_1.ChannelPool(config.network, kv);
115
+ const pool = new pool_1.ChannelPool(config.network, kv, config.lockTtlSeconds);
72
116
  const networkPassphrase = (0, config_1.getNetworkPassphrase)(config.network);
73
- try {
74
- // 1. Validate and parse request (xdr OR func+auth)
75
- const request = (0, validation_1.validateAndParseRequest)(params);
76
- console.debug(`[channels] Request type: ${request.type}, auth entries: ${request.type === "func-auth" ? request.auth.length : "N/A"}`);
77
- // 2. Get fund relayer
78
- const fundRelayer = api.useRelayer(config.fundRelayerId);
79
- const fundInfo = await fundRelayer.getRelayer();
80
- if (!fundInfo || !fundInfo.address) {
81
- throw (0, relayer_sdk_1.pluginError)("Fund relayer not found", {
82
- code: "RELAYER_UNAVAILABLE",
83
- status: constants_1.HTTP_STATUS.BAD_GATEWAY,
84
- details: { relayerId: config.fundRelayerId },
85
- });
86
- }
87
- const fundStatus = await fundRelayer.getRelayerStatus();
88
- if (fundStatus.network_type !== "stellar") {
89
- throw (0, relayer_sdk_1.pluginError)("Fund relayer network type must be stellar", {
90
- code: "UNSUPPORTED_NETWORK",
91
- status: constants_1.HTTP_STATUS.BAD_REQUEST,
92
- details: {
93
- network_type: fundStatus.network_type,
94
- relayerId: config.fundRelayerId,
95
- },
96
- });
97
- }
98
- // 3. Branch by request type
99
- if (request.type === "xdr") {
100
- console.log(`[channels] Flow: XDR submit-only`);
101
- return await handleXdrSubmit(request.xdr, fundRelayer, config.network, networkPassphrase, api);
102
- }
103
- console.log(`[channels] Flow: func+auth with channel account`);
104
- return await handleFuncAuthSubmit(request.func, request.auth, api, pool, fundRelayer, fundInfo.address, config.network, networkPassphrase);
117
+ // Fee tracking setup
118
+ let tracker;
119
+ const apiKey = getApiKey(headers, config.apiKeyHeader);
120
+ // If default limit is set, require API key
121
+ if (config.feeLimit !== undefined && !apiKey) {
122
+ throw (0, relayer_sdk_1.pluginError)("API key required", {
123
+ code: "API_KEY_REQUIRED",
124
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
125
+ });
105
126
  }
106
- finally {
107
- // Nothing to cleanup here; func-auth path releases locks internally
127
+ // Create tracker if API key is present (for tracking and custom limits)
128
+ if (apiKey) {
129
+ tracker = new fee_tracking_1.FeeTracker({
130
+ kv,
131
+ network: config.network,
132
+ apiKey,
133
+ defaultLimit: config.feeLimit,
134
+ resetPeriodMs: config.feeResetPeriodMs,
135
+ });
136
+ }
137
+ // 1. Validate and parse request (xdr OR func+auth)
138
+ const request = (0, validation_1.validateAndParseRequest)(params);
139
+ console.debug(`[channels] Request type: ${request.type}, auth entries: ${request.type === "func-auth" ? request.auth.length : "N/A"}`);
140
+ // 2. Get fund relayer
141
+ const fundRelayer = api.useRelayer(config.fundRelayerId);
142
+ const fundInfo = await fundRelayer.getRelayer();
143
+ if (!fundInfo || !fundInfo.address) {
144
+ throw (0, relayer_sdk_1.pluginError)("Fund relayer not found", {
145
+ code: "RELAYER_UNAVAILABLE",
146
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
147
+ details: { relayerId: config.fundRelayerId },
148
+ });
149
+ }
150
+ if (fundInfo.network_type !== "stellar") {
151
+ throw (0, relayer_sdk_1.pluginError)("Fund relayer network type must be stellar", {
152
+ code: "UNSUPPORTED_NETWORK",
153
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
154
+ details: {
155
+ network_type: fundInfo.network_type,
156
+ relayerId: config.fundRelayerId,
157
+ },
158
+ });
159
+ }
160
+ // 3. Branch by request type
161
+ if (request.type === "xdr") {
162
+ console.log(`[channels] Flow: XDR submit-only`);
163
+ return await handleXdrSubmit(request.xdr, fundRelayer, fundInfo.address, config.network, networkPassphrase, api, pool, tracker);
108
164
  }
165
+ console.log(`[channels] Flow: func+auth with channel account`);
166
+ return await handleFuncAuthSubmit(request.func, request.auth, api, pool, fundRelayer, fundInfo.address, config.network, networkPassphrase, tracker);
109
167
  }
110
168
  /**
111
169
  * Main plugin handler exported for OpenZeppelin Relayer
112
170
  */
113
171
  async function handler(context) {
114
- const result = await channelAccounts(context);
115
- return result;
172
+ return channelAccounts(context);
116
173
  }
@@ -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;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,2BAA2B,CAAC;AAe9E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAOxD;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAiC3E"}
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"}