@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.
- package/README.md +189 -2
- 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 +86 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/types.d.ts +60 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/plugin/config.d.ts +5 -8
- package/dist/plugin/config.d.ts.map +1 -1
- package/dist/plugin/config.js +46 -29
- package/dist/plugin/constants.d.ts +2 -3
- package/dist/plugin/constants.d.ts.map +1 -1
- package/dist/plugin/constants.js +2 -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 +7 -3
- package/dist/plugin/fee.d.ts.map +1 -1
- package/dist/plugin/fee.js +34 -12
- package/dist/plugin/handler.d.ts +9 -0
- package/dist/plugin/handler.d.ts.map +1 -1
- package/dist/plugin/handler.js +99 -42
- 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 +2 -0
- package/dist/plugin/simulation.d.ts.map +1 -1
- package/dist/plugin/simulation.js +32 -13
- package/dist/plugin/submit.d.ts +4 -1
- package/dist/plugin/submit.d.ts.map +1 -1
- package/dist/plugin/submit.js +25 -4
- package/package.json +13 -13
|
@@ -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;;CAGN,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,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:
|
|
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;
|
package/dist/plugin/fee.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fee.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - For Soroban transactions: use resourceFee +
|
|
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
|
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;;;;;;;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"}
|
package/dist/plugin/fee.js
CHANGED
|
@@ -2,14 +2,41 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* fee.ts
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - For Soroban transactions: use resourceFee +
|
|
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
|
|
49
|
+
const inclusionFee = getInclusionFee(transaction);
|
|
23
50
|
const fee = resourceFee > 0n
|
|
24
|
-
? resourceFee + BigInt(
|
|
25
|
-
: BigInt(constants_1.FEE.NON_SOROBAN_FEE +
|
|
26
|
-
console.debug(`[channels] Calculated max_fee: ${Number(fee)} stroops (resourceFee: ${resourceFee},
|
|
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
|
-
}
|
package/dist/plugin/handler.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/plugin/handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
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"}
|