@openzeppelin/relayer-plugin-channels 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +504 -0
  3. package/dist/build.d.ts +21 -0
  4. package/dist/build.d.ts.map +1 -0
  5. package/dist/build.js +100 -0
  6. package/dist/config.d.ts +31 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +89 -0
  9. package/dist/constants.d.ts +46 -0
  10. package/dist/constants.d.ts.map +1 -0
  11. package/dist/constants.js +56 -0
  12. package/dist/fee.d.ts +11 -0
  13. package/dist/fee.d.ts.map +1 -0
  14. package/dist/fee.js +41 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +115 -0
  18. package/dist/management.d.ts +11 -0
  19. package/dist/management.d.ts.map +1 -0
  20. package/dist/management.js +166 -0
  21. package/dist/pool.d.ts +31 -0
  22. package/dist/pool.d.ts.map +1 -0
  23. package/dist/pool.js +129 -0
  24. package/dist/simulation.d.ts +14 -0
  25. package/dist/simulation.d.ts.map +1 -0
  26. package/dist/simulation.js +52 -0
  27. package/dist/submit.d.ts +20 -0
  28. package/dist/submit.d.ts.map +1 -0
  29. package/dist/submit.js +102 -0
  30. package/dist/test/config.test.d.ts +2 -0
  31. package/dist/test/config.test.d.ts.map +1 -0
  32. package/dist/test/config.test.js +43 -0
  33. package/dist/test/fee.test.d.ts +2 -0
  34. package/dist/test/fee.test.d.ts.map +1 -0
  35. package/dist/test/fee.test.js +34 -0
  36. package/dist/test/helpers/fakeKV.d.ts +17 -0
  37. package/dist/test/helpers/fakeKV.d.ts.map +1 -0
  38. package/dist/test/helpers/fakeKV.js +60 -0
  39. package/dist/test/management.test.d.ts +2 -0
  40. package/dist/test/management.test.d.ts.map +1 -0
  41. package/dist/test/management.test.js +66 -0
  42. package/dist/test/pool.busy.test.d.ts +2 -0
  43. package/dist/test/pool.busy.test.d.ts.map +1 -0
  44. package/dist/test/pool.busy.test.js +23 -0
  45. package/dist/test/pool.test.d.ts +2 -0
  46. package/dist/test/pool.test.d.ts.map +1 -0
  47. package/dist/test/pool.test.js +29 -0
  48. package/dist/test/tx.test.d.ts +2 -0
  49. package/dist/test/tx.test.d.ts.map +1 -0
  50. package/dist/test/tx.test.js +29 -0
  51. package/dist/test/validation.test.d.ts +2 -0
  52. package/dist/test/validation.test.d.ts.map +1 -0
  53. package/dist/test/validation.test.js +31 -0
  54. package/dist/tx.d.ts +8 -0
  55. package/dist/tx.d.ts.map +1 -0
  56. package/dist/tx.js +45 -0
  57. package/dist/types.d.ts +66 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +7 -0
  60. package/dist/validation.d.ts +9 -0
  61. package/dist/validation.d.ts.map +1 -0
  62. package/dist/validation.js +84 -0
  63. package/package.json +63 -0
package/dist/config.js ADDED
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * config.ts
4
+ *
5
+ * Environment-driven configuration for the channel accounts plugin.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.loadConfig = loadConfig;
9
+ exports.getNetworkPassphrase = getNetworkPassphrase;
10
+ exports.getLockTtlSeconds = getLockTtlSeconds;
11
+ exports.getMaxFee = getMaxFee;
12
+ exports.getAdminSecret = getAdminSecret;
13
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
14
+ const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
15
+ const constants_1 = require("./constants");
16
+ function requireEnv(name) {
17
+ const v = process.env[name];
18
+ if (!v || v.trim() === "") {
19
+ throw (0, relayer_sdk_1.pluginError)(`Missing required environment variable: ${name}`, {
20
+ code: "CONFIG_MISSING",
21
+ status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
22
+ details: { name },
23
+ });
24
+ }
25
+ return v.trim();
26
+ }
27
+ /**
28
+ * Load configuration from environment variables
29
+ */
30
+ function loadConfig() {
31
+ const networkRaw = requireEnv("STELLAR_NETWORK").toLowerCase();
32
+ if (networkRaw !== "testnet" && networkRaw !== "mainnet") {
33
+ throw (0, relayer_sdk_1.pluginError)('STELLAR_NETWORK must be "testnet" or "mainnet"', {
34
+ code: "UNSUPPORTED_NETWORK",
35
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
36
+ });
37
+ }
38
+ const fundRelayerId = requireEnv("FUND_RELAYER_ID");
39
+ const rpcUrl = requireEnv("SOROBAN_RPC_URL");
40
+ return {
41
+ fundRelayerId,
42
+ network: networkRaw,
43
+ rpcUrl,
44
+ };
45
+ }
46
+ /**
47
+ * Get the network passphrase based on the configuration
48
+ */
49
+ function getNetworkPassphrase(network) {
50
+ return network === "mainnet" ? stellar_sdk_1.Networks.PUBLIC : stellar_sdk_1.Networks.TESTNET;
51
+ }
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
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * constants.ts
3
+ *
4
+ * Centralized constants for the channel accounts plugin.
5
+ */
6
+ export declare const HTTP_STATUS: {
7
+ readonly BAD_REQUEST: 400;
8
+ readonly UNAUTHORIZED: 401;
9
+ readonly FORBIDDEN: 403;
10
+ readonly CONFLICT: 409;
11
+ readonly INTERNAL_SERVER_ERROR: 500;
12
+ readonly BAD_GATEWAY: 502;
13
+ readonly SERVICE_UNAVAILABLE: 503;
14
+ readonly GATEWAY_TIMEOUT: 504;
15
+ };
16
+ export declare const CONFIG: {
17
+ readonly DEFAULT_LOCK_TTL_SECONDS: 30;
18
+ readonly MIN_LOCK_TTL_SECONDS: 10;
19
+ readonly MAX_LOCK_TTL_SECONDS: 30;
20
+ readonly DEFAULT_MAX_FEE: 1000000;
21
+ };
22
+ export declare const POOL: {
23
+ readonly MUTEX_TTL_SECONDS: 1;
24
+ readonly MUTEX_MAX_SPINS: 30;
25
+ readonly MUTEX_RETRY_MIN_MS: 10;
26
+ readonly MUTEX_RETRY_MAX_MS: 30;
27
+ };
28
+ export declare const TIME: {
29
+ readonly MAX_TIME_BOUND_OFFSET_SECONDS: 30;
30
+ };
31
+ export declare const SIMULATION: {
32
+ readonly DEFAULT_FEE: "100";
33
+ readonly MIN_TIME_BOUND: 0;
34
+ readonly MAX_TIME_BOUND_OFFSET_SECONDS: 30;
35
+ readonly MAX_FUTURE_TIME_BOUND_SECONDS: 30;
36
+ };
37
+ export declare const POLLING: {
38
+ readonly INTERVAL_MS: 1000;
39
+ readonly TIMEOUT_MS: 25000;
40
+ };
41
+ export declare const FEE: {
42
+ readonly MIN_BASE_FEE: 205;
43
+ readonly MAX_BASE_FEE: 605;
44
+ readonly RESOURCE_FEE_OFFSET: 60000;
45
+ };
46
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,WAAW;;;;;;;;;CASd,CAAC;AAGX,eAAO,MAAM,MAAM;;;;;CAKT,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;;;;CAIN,CAAC"}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /**
3
+ * constants.ts
4
+ *
5
+ * Centralized constants for the channel accounts plugin.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.FEE = exports.POLLING = exports.SIMULATION = exports.TIME = exports.POOL = exports.CONFIG = exports.HTTP_STATUS = void 0;
9
+ // HTTP Status Codes
10
+ exports.HTTP_STATUS = {
11
+ BAD_REQUEST: 400,
12
+ UNAUTHORIZED: 401,
13
+ FORBIDDEN: 403,
14
+ CONFLICT: 409,
15
+ INTERNAL_SERVER_ERROR: 500,
16
+ BAD_GATEWAY: 502,
17
+ SERVICE_UNAVAILABLE: 503,
18
+ GATEWAY_TIMEOUT: 504,
19
+ };
20
+ // Configuration Constants
21
+ exports.CONFIG = {
22
+ DEFAULT_LOCK_TTL_SECONDS: 30,
23
+ MIN_LOCK_TTL_SECONDS: 10,
24
+ MAX_LOCK_TTL_SECONDS: 30,
25
+ DEFAULT_MAX_FEE: 1000000,
26
+ };
27
+ // Pool Constants
28
+ exports.POOL = {
29
+ // TTL for the short global mutex guarding acquire()
30
+ MUTEX_TTL_SECONDS: 1,
31
+ // Retry policy when mutex is busy
32
+ MUTEX_MAX_SPINS: 30,
33
+ MUTEX_RETRY_MIN_MS: 10,
34
+ MUTEX_RETRY_MAX_MS: 30,
35
+ };
36
+ // Time Constants
37
+ exports.TIME = {
38
+ MAX_TIME_BOUND_OFFSET_SECONDS: 30,
39
+ };
40
+ // Simulation-related defaults
41
+ exports.SIMULATION = {
42
+ DEFAULT_FEE: "100",
43
+ MIN_TIME_BOUND: 0,
44
+ MAX_TIME_BOUND_OFFSET_SECONDS: 30,
45
+ MAX_FUTURE_TIME_BOUND_SECONDS: 30,
46
+ };
47
+ // Polling for transactionWait
48
+ exports.POLLING = {
49
+ INTERVAL_MS: 1000,
50
+ TIMEOUT_MS: 25000,
51
+ };
52
+ exports.FEE = {
53
+ MIN_BASE_FEE: 205,
54
+ MAX_BASE_FEE: 605,
55
+ RESOURCE_FEE_OFFSET: 60000,
56
+ };
package/dist/fee.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * fee.ts
3
+ *
4
+ * Dynamic fee calculation for fee bump submissions.
5
+ * - For Soroban transactions, use resourceFee + random inclusion fee
6
+ * - For non-Soroban, add a fixed offset for safety
7
+ * - Optionally clamp to env MAX_FEE via config helper
8
+ */
9
+ import { Transaction } from "@stellar/stellar-sdk";
10
+ export declare function calculateMaxFee(transaction: Transaction): number;
11
+ //# sourceMappingURL=fee.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fee.d.ts","sourceRoot":"","sources":["../src/fee.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAIxD,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CA2BhE"}
package/dist/fee.js ADDED
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * fee.ts
4
+ *
5
+ * Dynamic fee calculation for fee bump submissions.
6
+ * - For Soroban transactions, use resourceFee + random inclusion fee
7
+ * - For non-Soroban, add a fixed offset for safety
8
+ * - Optionally clamp to env MAX_FEE via config helper
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.calculateMaxFee = calculateMaxFee;
12
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
13
+ const constants_1 = require("./constants");
14
+ const config_1 = require("./config");
15
+ function calculateMaxFee(transaction) {
16
+ const envelope = transaction.toEnvelope();
17
+ let resourceFee = 0n;
18
+ if (envelope.switch() === stellar_sdk_1.xdr.EnvelopeType.envelopeTypeTx()) {
19
+ const sorobanData = envelope.v1().tx().ext().sorobanData();
20
+ if (sorobanData) {
21
+ resourceFee = sorobanData.resourceFee().toBigInt();
22
+ }
23
+ }
24
+ const baseInclusion = getRandomInt(constants_1.FEE.MIN_BASE_FEE, constants_1.FEE.MAX_BASE_FEE);
25
+ let dynamic = resourceFee > 0n
26
+ ? resourceFee + BigInt(baseInclusion)
27
+ : BigInt(constants_1.FEE.RESOURCE_FEE_OFFSET + baseInclusion);
28
+ // Optional cap from env
29
+ const cap = (0, config_1.getMaxFee)();
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);
36
+ }
37
+ function getRandomInt(min, max) {
38
+ const mn = Math.ceil(min);
39
+ const mx = Math.floor(max);
40
+ return Math.floor(Math.random() * (mx - mn + 1)) + mn;
41
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * index.ts
3
+ *
4
+ * Main entry point for the channel accounts plugin.
5
+ * Orchestrates the transaction processing pipeline using channel accounts with fee bumping.
6
+ */
7
+ import { PluginContext } from "@openzeppelin/relayer-sdk";
8
+ export declare function handler(context: PluginContext): Promise<any>;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AAiLvE,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAGlE"}
package/dist/index.js ADDED
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * index.ts
4
+ *
5
+ * Main entry point for the channel accounts plugin.
6
+ * Orchestrates the transaction processing pipeline using channel accounts with fee bumping.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.handler = handler;
10
+ const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
11
+ const pool_1 = require("./pool");
12
+ const config_1 = require("./config");
13
+ const validation_1 = require("./validation");
14
+ const management_1 = require("./management");
15
+ const submit_1 = require("./submit");
16
+ const constants_1 = require("./constants");
17
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
18
+ const simulation_1 = require("./simulation");
19
+ const fee_1 = require("./fee");
20
+ const tx_1 = require("./tx");
21
+ async function handleXdrSubmit(xdrStr, fundRelayer, network, networkPassphrase, api) {
22
+ const tx = new stellar_sdk_1.Transaction(xdrStr, networkPassphrase);
23
+ const validated = (0, tx_1.validateExistingTransactionForSubmitOnly)(tx);
24
+ const maxFee = (0, fee_1.calculateMaxFee)(validated);
25
+ return (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, validated.toXDR(), network, maxFee, api);
26
+ }
27
+ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddress, network, networkPassphrase, rpc) {
28
+ let poolLock;
29
+ try {
30
+ poolLock = await pool.acquire();
31
+ const channelRelayer = api.useRelayer(poolLock.relayerId);
32
+ const channelInfo = await channelRelayer.getRelayer();
33
+ console.log(`[channels] Acquired channel: ${poolLock.relayerId}`);
34
+ if (!channelInfo || !channelInfo.address) {
35
+ throw (0, relayer_sdk_1.pluginError)("Channel relayer not found", {
36
+ code: "RELAYER_UNAVAILABLE",
37
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
38
+ details: { relayerId: poolLock.relayerId },
39
+ });
40
+ }
41
+ const channelStatus = await channelRelayer.getRelayerStatus();
42
+ if (channelStatus.network_type !== "stellar") {
43
+ throw (0, relayer_sdk_1.pluginError)("Channel relayer network type must be stellar", {
44
+ code: "UNSUPPORTED_NETWORK",
45
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
46
+ details: {
47
+ network_type: channelStatus.network_type,
48
+ relayerId: poolLock.relayerId,
49
+ },
50
+ });
51
+ }
52
+ const built = await (0, simulation_1.simulateAndBuildWithChannel)(func, auth, { address: channelInfo.address, sequence: channelStatus.sequence_number }, fundAddress, rpc, networkPassphrase);
53
+ const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, fundRelayer, channelInfo.address, fundAddress, networkPassphrase);
54
+ const maxFee = (0, fee_1.calculateMaxFee)(signedTx);
55
+ return await (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, signedTx.toXDR(), network, maxFee, api);
56
+ }
57
+ finally {
58
+ if (poolLock) {
59
+ await pool.release(poolLock);
60
+ }
61
+ }
62
+ }
63
+ async function channelAccounts(context) {
64
+ const { api, kv, params } = context;
65
+ // Management branch: handle and return immediately
66
+ if ((0, management_1.isManagementRequest)(params)) {
67
+ return await (0, management_1.handleManagement)(context);
68
+ }
69
+ // Load config and initialize per-request dependencies
70
+ const config = (0, config_1.loadConfig)();
71
+ const pool = new pool_1.ChannelPool(config.network, kv);
72
+ const networkPassphrase = (0, config_1.getNetworkPassphrase)(config.network);
73
+ const rpc = new stellar_sdk_1.SorobanRpc.Server(config.rpcUrl);
74
+ try {
75
+ // 1. Validate and parse request (xdr OR func+auth)
76
+ const request = (0, validation_1.validateAndParseRequest)(params);
77
+ console.debug(`[channels] Request type: ${request.type}, auth entries: ${request.type === "func-auth" ? request.auth.length : "N/A"}`);
78
+ // 2. Get fund relayer
79
+ const fundRelayer = api.useRelayer(config.fundRelayerId);
80
+ const fundInfo = await fundRelayer.getRelayer();
81
+ if (!fundInfo || !fundInfo.address) {
82
+ throw (0, relayer_sdk_1.pluginError)("Fund relayer not found", {
83
+ code: "RELAYER_UNAVAILABLE",
84
+ status: constants_1.HTTP_STATUS.BAD_GATEWAY,
85
+ details: { relayerId: config.fundRelayerId },
86
+ });
87
+ }
88
+ const fundStatus = await fundRelayer.getRelayerStatus();
89
+ if (fundStatus.network_type !== "stellar") {
90
+ throw (0, relayer_sdk_1.pluginError)("Fund relayer network type must be stellar", {
91
+ code: "UNSUPPORTED_NETWORK",
92
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
93
+ details: {
94
+ network_type: fundStatus.network_type,
95
+ relayerId: config.fundRelayerId,
96
+ },
97
+ });
98
+ }
99
+ // 3. Branch by request type
100
+ if (request.type === "xdr") {
101
+ console.log(`[channels] Flow: XDR submit-only`);
102
+ return await handleXdrSubmit(request.xdr, fundRelayer, config.network, networkPassphrase, api);
103
+ }
104
+ 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, rpc);
106
+ }
107
+ finally {
108
+ // Nothing to cleanup here; func-auth path releases locks internally
109
+ }
110
+ }
111
+ // Error-catching wrapper and main export
112
+ async function handler(context) {
113
+ const result = await channelAccounts(context);
114
+ return result;
115
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * management.ts
3
+ *
4
+ * Payload-based management API for channel relayerIds.
5
+ * - listChannelAccounts: returns relayerIds from KV
6
+ * - setChannelAccounts: replaces relayerIds array in KV (checks lock conflicts)
7
+ */
8
+ import type { PluginContext } from "@openzeppelin/relayer-sdk";
9
+ export declare function isManagementRequest(params: any): boolean;
10
+ export declare function handleManagement(context: PluginContext): Promise<any>;
11
+ //# sourceMappingURL=management.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"management.d.ts","sourceRoot":"","sources":["../src/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"}
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ /**
3
+ * management.ts
4
+ *
5
+ * Payload-based management API for channel relayerIds.
6
+ * - listChannelAccounts: returns relayerIds from KV
7
+ * - setChannelAccounts: replaces relayerIds array in KV (checks lock conflicts)
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.isManagementRequest = isManagementRequest;
11
+ exports.handleManagement = handleManagement;
12
+ const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
13
+ const config_1 = require("./config");
14
+ const constants_1 = require("./constants");
15
+ function timingSafeEqual(a, b) {
16
+ // Basic constant-time comparison without crypto dep
17
+ if (a.length !== b.length)
18
+ return false;
19
+ let result = 0;
20
+ for (let i = 0; i < a.length; i++) {
21
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
22
+ }
23
+ return result === 0;
24
+ }
25
+ function isManagementRequest(params) {
26
+ return Boolean(params &&
27
+ typeof params === "object" &&
28
+ params.management &&
29
+ typeof params.management === "object");
30
+ }
31
+ async function handleManagement(context) {
32
+ const { kv, params } = context;
33
+ const adminSecretEnv = (0, config_1.getAdminSecret)();
34
+ if (!adminSecretEnv) {
35
+ throw (0, relayer_sdk_1.pluginError)("Management API disabled", {
36
+ code: "MANAGEMENT_DISABLED",
37
+ status: constants_1.HTTP_STATUS.FORBIDDEN,
38
+ });
39
+ }
40
+ const m = params?.management || {};
41
+ const provided = (m.adminSecret ?? "").toString();
42
+ if (!provided || !timingSafeEqual(provided, adminSecretEnv)) {
43
+ throw (0, relayer_sdk_1.pluginError)("Unauthorized", {
44
+ code: "UNAUTHORIZED",
45
+ status: constants_1.HTTP_STATUS.UNAUTHORIZED,
46
+ });
47
+ }
48
+ const action = String(m.action || "");
49
+ // Load config (requires env like STELLAR_NETWORK) after auth
50
+ const cfg = (0, config_1.loadConfig)();
51
+ switch (action) {
52
+ case "listChannelAccounts":
53
+ return await listChannelAccounts(kv, cfg.network);
54
+ case "setChannelAccounts":
55
+ return await setChannelAccounts(kv, cfg.network, m);
56
+ default:
57
+ throw (0, relayer_sdk_1.pluginError)("Invalid management action", {
58
+ code: "INVALID_ACTION",
59
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
60
+ });
61
+ }
62
+ }
63
+ async function listChannelAccounts(kv, network) {
64
+ const key = `${network}:channel:relayer-ids`;
65
+ try {
66
+ const doc = await kv.get?.(key);
67
+ const relayerIds = Array.isArray(doc?.relayerIds)
68
+ ? doc.relayerIds.map(normalizeId)
69
+ : [];
70
+ return { relayerIds };
71
+ }
72
+ catch (e) {
73
+ throw (0, relayer_sdk_1.pluginError)("KV error while listing channel accounts", {
74
+ code: "KV_ERROR",
75
+ status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
76
+ });
77
+ }
78
+ }
79
+ async function setChannelAccounts(kv, network, payload) {
80
+ const incoming = payload?.relayerIds;
81
+ if (!Array.isArray(incoming)) {
82
+ throw (0, relayer_sdk_1.pluginError)("Invalid payload: relayerIds must be an array", {
83
+ code: "INVALID_PAYLOAD",
84
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
85
+ });
86
+ }
87
+ // Normalize, validate, unique
88
+ const relayerIds = unique(incoming.map(normalizeId).filter(validRelayerId));
89
+ // Read current
90
+ const listKey = `${network}:channel:relayer-ids`;
91
+ const current = await readStoredRelayerIds(kv, listKey);
92
+ // Check for locked removals
93
+ const removals = current.filter((id) => !relayerIds.includes(id));
94
+ const locked = [];
95
+ for (const id of removals) {
96
+ if (await isRelayerIdLocked(kv, network, id)) {
97
+ locked.push(id);
98
+ }
99
+ }
100
+ if (locked.length > 0) {
101
+ throw (0, relayer_sdk_1.pluginError)("Locked relayer IDs cannot be removed", {
102
+ code: "LOCKED_CONFLICT",
103
+ status: constants_1.HTTP_STATUS.CONFLICT,
104
+ details: { locked },
105
+ });
106
+ }
107
+ // Write new list
108
+ try {
109
+ await kv.set(listKey, { relayerIds });
110
+ return { ok: true, appliedRelayerIds: relayerIds };
111
+ }
112
+ catch (e) {
113
+ throw (0, relayer_sdk_1.pluginError)("KV error while saving channel accounts", {
114
+ code: "KV_ERROR",
115
+ status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
116
+ });
117
+ }
118
+ }
119
+ function normalizeId(id) {
120
+ return String(id).trim().toLowerCase();
121
+ }
122
+ function validRelayerId(id) {
123
+ if (!id)
124
+ return false;
125
+ if (id.length > 128)
126
+ return false;
127
+ return /^[a-z0-9:_-]+$/.test(id);
128
+ }
129
+ function unique(arr) {
130
+ return Array.from(new Set(arr));
131
+ }
132
+ async function readStoredRelayerIds(kv, key) {
133
+ try {
134
+ const doc = await kv.get?.(key);
135
+ return Array.isArray(doc?.relayerIds)
136
+ ? doc.relayerIds.map(normalizeId)
137
+ : [];
138
+ }
139
+ catch (error) {
140
+ throw (0, relayer_sdk_1.pluginError)("KV error while reading channel accounts", {
141
+ code: "KV_ERROR",
142
+ status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
143
+ details: {
144
+ key,
145
+ message: error instanceof Error ? error.message : String(error),
146
+ },
147
+ });
148
+ }
149
+ }
150
+ async function isRelayerIdLocked(kv, network, id) {
151
+ const key = `${network}:channel:in-use:${id}`;
152
+ try {
153
+ return await kv.exists(key);
154
+ }
155
+ catch (error) {
156
+ throw (0, relayer_sdk_1.pluginError)("KV error while checking relayer lock", {
157
+ code: "KV_ERROR",
158
+ status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
159
+ details: {
160
+ relayerId: id,
161
+ key,
162
+ message: error instanceof Error ? error.message : String(error),
163
+ },
164
+ });
165
+ }
166
+ }
package/dist/pool.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * pool.ts
3
+ *
4
+ * KV-backed, stateless channel pool.
5
+ * - Membership comes from KV: <network>:channel:relayer-ids
6
+ * - Per-relayer locks with tokens: <network>:channel:in-use:<relayerId>
7
+ * - Uses a short global mutex to make acquire atomic across workers.
8
+ */
9
+ import { PluginKVStore } from "@openzeppelin/relayer-sdk";
10
+ export type PoolLock = {
11
+ relayerId: string;
12
+ token: string;
13
+ };
14
+ export declare class ChannelPool {
15
+ private readonly network;
16
+ private readonly globalLockKey;
17
+ private readonly channelLockTtlSec;
18
+ private readonly mutexTtlSec;
19
+ private readonly kv;
20
+ constructor(network: "testnet" | "mainnet", kv: PluginKVStore);
21
+ /** Acquire a relayerId with a token lock */
22
+ acquire(): Promise<PoolLock>;
23
+ private withGlobalMutex;
24
+ private tryLockAnyRelayer;
25
+ /** Release the lock if we own it */
26
+ release(lock: PoolLock): Promise<void>;
27
+ private membershipKey;
28
+ private lockKey;
29
+ private getRelayerIdsFromKV;
30
+ }
31
+ //# sourceMappingURL=pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AAKvE,MAAM,MAAM,QAAQ,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAI5D,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;gBAEvB,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,EAAE,EAAE,aAAa;IAQ7D,4CAA4C;IACtC,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC;YAuBpB,eAAe;YASf,iBAAiB;IAwB/B,oCAAoC;IAC9B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5C,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;YAID,mBAAmB;CAWlC"}