@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.
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/dist/build.d.ts +21 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +100 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +89 -0
- package/dist/constants.d.ts +46 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +56 -0
- package/dist/fee.d.ts +11 -0
- package/dist/fee.d.ts.map +1 -0
- package/dist/fee.js +41 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +115 -0
- package/dist/management.d.ts +11 -0
- package/dist/management.d.ts.map +1 -0
- package/dist/management.js +166 -0
- package/dist/pool.d.ts +31 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +129 -0
- package/dist/simulation.d.ts +14 -0
- package/dist/simulation.d.ts.map +1 -0
- package/dist/simulation.js +52 -0
- package/dist/submit.d.ts +20 -0
- package/dist/submit.d.ts.map +1 -0
- package/dist/submit.js +102 -0
- package/dist/test/config.test.d.ts +2 -0
- package/dist/test/config.test.d.ts.map +1 -0
- package/dist/test/config.test.js +43 -0
- package/dist/test/fee.test.d.ts +2 -0
- package/dist/test/fee.test.d.ts.map +1 -0
- package/dist/test/fee.test.js +34 -0
- package/dist/test/helpers/fakeKV.d.ts +17 -0
- package/dist/test/helpers/fakeKV.d.ts.map +1 -0
- package/dist/test/helpers/fakeKV.js +60 -0
- package/dist/test/management.test.d.ts +2 -0
- package/dist/test/management.test.d.ts.map +1 -0
- package/dist/test/management.test.js +66 -0
- package/dist/test/pool.busy.test.d.ts +2 -0
- package/dist/test/pool.busy.test.d.ts.map +1 -0
- package/dist/test/pool.busy.test.js +23 -0
- package/dist/test/pool.test.d.ts +2 -0
- package/dist/test/pool.test.d.ts.map +1 -0
- package/dist/test/pool.test.js +29 -0
- package/dist/test/tx.test.d.ts +2 -0
- package/dist/test/tx.test.d.ts.map +1 -0
- package/dist/test/tx.test.js +29 -0
- package/dist/test/validation.test.d.ts +2 -0
- package/dist/test/validation.test.d.ts.map +1 -0
- package/dist/test/validation.test.js +31 -0
- package/dist/tx.d.ts +8 -0
- package/dist/tx.d.ts.map +1 -0
- package/dist/tx.js +45 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/validation.d.ts +9 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +84 -0
- 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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|