@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/pool.js ADDED
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ /**
3
+ * pool.ts
4
+ *
5
+ * KV-backed, stateless channel pool.
6
+ * - Membership comes from KV: <network>:channel:relayer-ids
7
+ * - Per-relayer locks with tokens: <network>:channel:in-use:<relayerId>
8
+ * - Uses a short global mutex to make acquire atomic across workers.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.ChannelPool = void 0;
15
+ const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
16
+ const crypto_1 = __importDefault(require("crypto"));
17
+ const config_1 = require("./config");
18
+ const constants_1 = require("./constants");
19
+ class ChannelPool {
20
+ constructor(network, kv) {
21
+ this.network = network;
22
+ this.kv = kv;
23
+ this.globalLockKey = `${this.network}:channel-pool-lock`;
24
+ this.channelLockTtlSec = (0, config_1.getLockTtlSeconds)();
25
+ this.mutexTtlSec = constants_1.POOL.MUTEX_TTL_SECONDS;
26
+ }
27
+ /** Acquire a relayerId with a token lock */
28
+ async acquire() {
29
+ const maxSpins = constants_1.POOL.MUTEX_MAX_SPINS;
30
+ for (let i = 0; i < maxSpins; i++) {
31
+ const r = await this.withGlobalMutex(() => this.tryLockAnyRelayer());
32
+ if (r === null) {
33
+ const jitter = constants_1.POOL.MUTEX_RETRY_MIN_MS +
34
+ Math.floor(Math.random() *
35
+ (constants_1.POOL.MUTEX_RETRY_MAX_MS - constants_1.POOL.MUTEX_RETRY_MIN_MS + 1));
36
+ await sleep(jitter);
37
+ continue;
38
+ }
39
+ return r;
40
+ }
41
+ throw (0, relayer_sdk_1.pluginError)("Too many transactions queued. Please try again later", {
42
+ code: "POOL_CAPACITY",
43
+ status: constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE,
44
+ });
45
+ }
46
+ // Run a function under the short-lived global mutex; returns null if busy
47
+ async withGlobalMutex(fn) {
48
+ const r = (await this.kv.withLock(this.globalLockKey, fn, {
49
+ ttlSec: this.mutexTtlSec,
50
+ onBusy: "skip",
51
+ }));
52
+ return r;
53
+ }
54
+ // Inside the mutex: pick an available relayer and set its channel lock
55
+ async tryLockAnyRelayer() {
56
+ const ids = await this.getRelayerIdsFromKV();
57
+ if (ids.length === 0) {
58
+ throw (0, relayer_sdk_1.pluginError)("No channel accounts configured. Use the management API to set channel accounts.", {
59
+ code: "NO_CHANNELS_CONFIGURED",
60
+ status: constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE,
61
+ });
62
+ }
63
+ shuffle(ids);
64
+ for (const relayerId of ids) {
65
+ const key = this.lockKey(relayerId);
66
+ const exists = await this.kv.exists(key);
67
+ if (exists)
68
+ continue;
69
+ const token = randomToken();
70
+ const entry = { token, lockedAt: new Date().toISOString() };
71
+ await this.kv.set(key, entry, { ttlSec: this.channelLockTtlSec });
72
+ return { relayerId, token };
73
+ }
74
+ return null;
75
+ }
76
+ /** Release the lock if we own it */
77
+ async release(lock) {
78
+ try {
79
+ const key = this.lockKey(lock.relayerId);
80
+ const current = await this.kv.get(key);
81
+ if (current?.token === lock.token) {
82
+ await this.kv.del(key);
83
+ }
84
+ }
85
+ catch {
86
+ // ignore release errors
87
+ }
88
+ }
89
+ membershipKey() {
90
+ return `${this.network}:channel:relayer-ids`;
91
+ }
92
+ lockKey(relayerId) {
93
+ return `${this.network}:channel:in-use:${relayerId}`;
94
+ }
95
+ async getRelayerIdsFromKV() {
96
+ try {
97
+ const doc = await this.kv.get(this.membershipKey());
98
+ if (!doc || !Array.isArray(doc.relayerIds))
99
+ return [];
100
+ // Normalize and unique
101
+ const set = new Set(doc.relayerIds.map(normalizeId));
102
+ return Array.from(set.values());
103
+ }
104
+ catch {
105
+ return [];
106
+ }
107
+ }
108
+ }
109
+ exports.ChannelPool = ChannelPool;
110
+ function shuffle(arr) {
111
+ for (let i = arr.length - 1; i > 0; i--) {
112
+ const j = Math.floor(Math.random() * (i + 1));
113
+ [arr[i], arr[j]] = [arr[j], arr[i]];
114
+ }
115
+ }
116
+ function randomToken() {
117
+ try {
118
+ return crypto_1.default.randomBytes(16).toString("hex");
119
+ }
120
+ catch {
121
+ return (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2));
122
+ }
123
+ }
124
+ function normalizeId(id) {
125
+ return String(id).trim().toLowerCase();
126
+ }
127
+ function sleep(ms) {
128
+ return new Promise((r) => setTimeout(r, ms));
129
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * simulation.ts
3
+ *
4
+ * Build and simulate a Soroban transaction using a channel account as the source
5
+ * and the fund account as the operation source. Returns an unsigned inner
6
+ * transaction with sorobanData and correct fee set to the resource fee.
7
+ */
8
+ import { SorobanRpc, Transaction, xdr } from "@stellar/stellar-sdk";
9
+ export interface ChannelAccount {
10
+ address: string;
11
+ sequence: string;
12
+ }
13
+ export declare function simulateAndBuildWithChannel(func: xdr.HostFunction, auth: xdr.SorobanAuthorizationEntry[] | undefined, channel: ChannelAccount, _fundAddress: string, rpc: SorobanRpc.Server, networkPassphrase: string): Promise<Transaction>;
14
+ //# sourceMappingURL=simulation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simulation.d.ts","sourceRoot":"","sources":["../src/simulation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAGL,UAAU,EACV,WAAW,EAEX,GAAG,EACJ,MAAM,sBAAsB,CAAC;AAI9B,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,UAAU,CAAC,MAAM,EACtB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,WAAW,CAAC,CA8CtB"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * simulation.ts
4
+ *
5
+ * Build and simulate a Soroban transaction using a channel account as the source
6
+ * and the fund account as the operation source. Returns an unsigned inner
7
+ * transaction with sorobanData and correct fee set to the resource fee.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.simulateAndBuildWithChannel = simulateAndBuildWithChannel;
11
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
12
+ const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
13
+ const constants_1 = require("./constants");
14
+ async function simulateAndBuildWithChannel(func, auth, channel, _fundAddress, rpc, networkPassphrase) {
15
+ const now = Math.floor(Date.now() / 1000);
16
+ console.debug(`[channels] Building tx: channel=${channel.address}, seq=${channel.sequence}, auth_count=${auth?.length ?? 0}`);
17
+ // Build inner transaction (source = channel, op source = fund)
18
+ const transaction = new stellar_sdk_1.TransactionBuilder(new stellar_sdk_1.Account(channel.address, channel.sequence), {
19
+ fee: constants_1.SIMULATION.DEFAULT_FEE,
20
+ networkPassphrase,
21
+ timebounds: {
22
+ minTime: constants_1.SIMULATION.MIN_TIME_BOUND,
23
+ maxTime: now + constants_1.SIMULATION.MAX_TIME_BOUND_OFFSET_SECONDS,
24
+ },
25
+ })
26
+ .addOperation(stellar_sdk_1.Operation.invokeHostFunction({
27
+ func,
28
+ auth,
29
+ // No explicit source: default to transaction source (channel account)
30
+ }))
31
+ .build();
32
+ // Prepare transaction (attaches sorobanData/resources, preserves provided auth)
33
+ try {
34
+ const prepared = await rpc.prepareTransaction(transaction);
35
+ const resourceFee = prepared
36
+ .toEnvelope()
37
+ .v1()
38
+ .tx()
39
+ .ext()
40
+ .sorobanData()
41
+ ?.resourceFee();
42
+ console.debug(`[channels] Simulation complete: resourceFee=${resourceFee}`);
43
+ return prepared;
44
+ }
45
+ catch (e) {
46
+ throw (0, relayer_sdk_1.pluginError)("Simulation failed", {
47
+ code: "SIMULATION_FAILED",
48
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
49
+ details: { error: e instanceof Error ? e.message : String(e) },
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * submit.ts
3
+ *
4
+ * Signing and submission logic for channel account transactions.
5
+ */
6
+ import { Transaction } from "@stellar/stellar-sdk";
7
+ import { Relayer, PluginAPI } from "@openzeppelin/relayer-sdk";
8
+ import { ChannelAccountsResponse } from "./types";
9
+ /**
10
+ * Sign transaction with both channel and fund relayers
11
+ * - First sign with channel account
12
+ * - Then sign with fund account
13
+ * - Both signatures are added to the transaction
14
+ */
15
+ export declare function signWithChannelAndFund(transaction: Transaction, channelRelayer: Relayer, _fundRelayer: Relayer, channelAddress: string, _fundAddress: string, networkPassphrase: string): Promise<Transaction>;
16
+ /**
17
+ * Submit transaction with fee bump and wait for confirmation
18
+ */
19
+ export declare function submitWithFeeBumpAndWait(fundRelayer: Relayer, signedXdr: string, network: "testnet" | "mainnet", maxFee: number, api: PluginAPI): Promise<ChannelAccountsResponse>;
20
+ //# sourceMappingURL=submit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submit.d.ts","sourceRoot":"","sources":["../src/submit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAEL,OAAO,EAGP,SAAS,EACV,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,OAAO,EACvB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,WAAW,CAAC,CA2BtB;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,WAAW,EAAE,OAAO,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,uBAAuB,CAAC,CAwDlC"}
package/dist/submit.js ADDED
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * submit.ts
4
+ *
5
+ * Signing and submission logic for channel account transactions.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.signWithChannelAndFund = signWithChannelAndFund;
9
+ exports.submitWithFeeBumpAndWait = submitWithFeeBumpAndWait;
10
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
11
+ const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
12
+ const constants_1 = require("./constants");
13
+ /**
14
+ * Sign transaction with both channel and fund relayers
15
+ * - First sign with channel account
16
+ * - Then sign with fund account
17
+ * - Both signatures are added to the transaction
18
+ */
19
+ async function signWithChannelAndFund(transaction, channelRelayer, _fundRelayer, channelAddress, _fundAddress, networkPassphrase) {
20
+ const txXdr = transaction.toXDR();
21
+ console.debug(`[channels] Signing transaction with channel (${channelAddress})`);
22
+ // Get signatures from both accounts sequentially
23
+ // Channel signs first
24
+ const channelSignResult = await channelRelayer.signTransaction({
25
+ unsigned_xdr: txXdr,
26
+ });
27
+ if (!isSignTransactionResponseStellar(channelSignResult)) {
28
+ throw (0, relayer_sdk_1.pluginError)("Invalid channel signature response", {
29
+ code: "INVALID_SIGNATURE",
30
+ status: constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR,
31
+ });
32
+ }
33
+ // Add both signatures to the transaction
34
+ const signedTx = new stellar_sdk_1.Transaction(txXdr, networkPassphrase);
35
+ signedTx.addSignature(channelAddress, channelSignResult.signature);
36
+ console.debug(`[channels] Transaction signed: ${signedTx.signatures.length} signature(s) added`);
37
+ return signedTx;
38
+ }
39
+ /**
40
+ * Submit transaction with fee bump and wait for confirmation
41
+ */
42
+ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee, api) {
43
+ // Submit with fee bump
44
+ console.debug(`[channels] Sending fee bump tx: network=${network}, maxFee=${maxFee}, xdr_len=${signedXdr.length}`);
45
+ const payload = {
46
+ network,
47
+ transaction_xdr: signedXdr,
48
+ fee_bump: true,
49
+ max_fee: maxFee,
50
+ };
51
+ console.debug(`[channels] Relayer payload: ${JSON.stringify(payload)}`);
52
+ const submission = await fundRelayer.sendTransaction(payload);
53
+ // Wait for confirmation
54
+ try {
55
+ const final = (await api.transactionWait(submission, {
56
+ interval: 500,
57
+ timeout: 25000,
58
+ }));
59
+ // Check if transaction actually succeeded
60
+ if (final.status === "failed") {
61
+ throw (0, relayer_sdk_1.pluginError)(final.status_reason || "Transaction failed", {
62
+ code: "ONCHAIN_FAILED",
63
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
64
+ details: {
65
+ status: String(final.status),
66
+ reason: final.status_reason ?? null,
67
+ id: final.id,
68
+ hash: final.hash ?? null,
69
+ },
70
+ });
71
+ }
72
+ return {
73
+ transactionId: final.id,
74
+ status: final.status,
75
+ hash: final.hash ?? null,
76
+ };
77
+ }
78
+ catch (error) {
79
+ // If it's already a pluginError with ONCHAIN_FAILED, rethrow it
80
+ if (error.code === "ONCHAIN_FAILED") {
81
+ throw error;
82
+ }
83
+ // Otherwise, it's a timeout
84
+ throw (0, relayer_sdk_1.pluginError)("Transaction wait timeout. It may still submit.", {
85
+ code: "WAIT_TIMEOUT",
86
+ status: constants_1.HTTP_STATUS.GATEWAY_TIMEOUT,
87
+ details: {
88
+ id: submission.id,
89
+ hash: submission.hash ?? null,
90
+ },
91
+ });
92
+ }
93
+ }
94
+ /**
95
+ * Type guard for SignTransactionResponseStellar
96
+ */
97
+ function isSignTransactionResponseStellar(data) {
98
+ return (data !== null &&
99
+ typeof data === "object" &&
100
+ "signature" in data &&
101
+ "signedXdr" in data);
102
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../src/test/config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
5
+ const config_1 = require("../config");
6
+ const env = process.env;
7
+ (0, vitest_1.describe)("config", () => {
8
+ (0, vitest_1.beforeEach)(() => {
9
+ process.env = { ...env };
10
+ });
11
+ (0, vitest_1.afterEach)(() => {
12
+ process.env = env;
13
+ });
14
+ (0, vitest_1.test)("loadConfig reads required env", () => {
15
+ process.env.STELLAR_NETWORK = "testnet";
16
+ process.env.SOROBAN_RPC_URL = "http://localhost:9999";
17
+ process.env.FUND_RELAYER_ID = "fund-relayer";
18
+ const cfg = (0, config_1.loadConfig)();
19
+ (0, vitest_1.expect)(cfg.network).toBe("testnet");
20
+ (0, vitest_1.expect)(cfg.rpcUrl).toBe("http://localhost:9999");
21
+ (0, vitest_1.expect)(cfg.fundRelayerId).toBe("fund-relayer");
22
+ });
23
+ (0, vitest_1.test)("network passphrase", () => {
24
+ (0, vitest_1.expect)((0, config_1.getNetworkPassphrase)("testnet")).toBe(stellar_sdk_1.Networks.TESTNET);
25
+ (0, vitest_1.expect)((0, config_1.getNetworkPassphrase)("mainnet")).toBe(stellar_sdk_1.Networks.PUBLIC);
26
+ });
27
+ (0, vitest_1.test)("lock ttl bounds", () => {
28
+ delete process.env.LOCK_TTL_SECONDS;
29
+ (0, vitest_1.expect)((0, config_1.getLockTtlSeconds)()).toBe(30);
30
+ process.env.LOCK_TTL_SECONDS = "5";
31
+ (0, vitest_1.expect)((0, config_1.getLockTtlSeconds)()).toBe(30);
32
+ process.env.LOCK_TTL_SECONDS = "10";
33
+ (0, vitest_1.expect)((0, config_1.getLockTtlSeconds)()).toBe(10);
34
+ process.env.LOCK_TTL_SECONDS = "29";
35
+ (0, vitest_1.expect)((0, config_1.getLockTtlSeconds)()).toBe(29);
36
+ });
37
+ (0, vitest_1.test)("max fee env", () => {
38
+ delete process.env.MAX_FEE;
39
+ (0, vitest_1.expect)(typeof (0, config_1.getMaxFee)()).toBe("number");
40
+ process.env.MAX_FEE = "12345";
41
+ (0, vitest_1.expect)((0, config_1.getMaxFee)()).toBe(12345);
42
+ });
43
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fee.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fee.test.d.ts","sourceRoot":"","sources":["../../src/test/fee.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const fee_1 = require("../fee");
5
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
6
+ (0, vitest_1.describe)("fee", () => {
7
+ const passphrase = stellar_sdk_1.Networks.TESTNET;
8
+ function buildSimpleTx() {
9
+ const acc = new stellar_sdk_1.Account("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "1");
10
+ return new stellar_sdk_1.TransactionBuilder(acc, {
11
+ fee: "100",
12
+ networkPassphrase: passphrase,
13
+ })
14
+ .setTimeout(30)
15
+ .build();
16
+ }
17
+ (0, vitest_1.test)("non-soroban uses offset + base", () => {
18
+ const orig = Math.random;
19
+ Math.random = () => 0; // pick min base fee
20
+ const tx = buildSimpleTx();
21
+ const fee = (0, fee_1.calculateMaxFee)(tx);
22
+ (0, vitest_1.expect)(typeof fee).toBe("number");
23
+ (0, vitest_1.expect)(fee).toBeGreaterThan(0);
24
+ Math.random = orig;
25
+ });
26
+ (0, vitest_1.test)("clamps to MAX_FEE when set", () => {
27
+ const tx = buildSimpleTx();
28
+ const old = process.env.MAX_FEE;
29
+ process.env.MAX_FEE = "1000";
30
+ const fee = (0, fee_1.calculateMaxFee)(tx);
31
+ (0, vitest_1.expect)(fee).toBeLessThanOrEqual(1000);
32
+ process.env.MAX_FEE = old;
33
+ });
34
+ });
@@ -0,0 +1,17 @@
1
+ import type { PluginKVStore } from "@openzeppelin/relayer-sdk";
2
+ export declare class FakeKV implements PluginKVStore {
3
+ private store;
4
+ get<T = any>(key: string): Promise<T | null>;
5
+ set(key: string, value: any, opts?: {
6
+ ttlSec?: number;
7
+ }): Promise<boolean>;
8
+ del(key: string): Promise<boolean>;
9
+ exists(key: string): Promise<boolean>;
10
+ withLock<T>(key: string, fn: () => Promise<T>, opts?: {
11
+ ttlSec?: number;
12
+ onBusy?: "throw" | "skip";
13
+ }): Promise<T | null>;
14
+ listKeys(_pattern?: string): Promise<string[]>;
15
+ clear(): Promise<number>;
16
+ }
17
+ //# sourceMappingURL=fakeKV.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakeKV.d.ts","sourceRoot":"","sources":["../../../src/test/helpers/fakeKV.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAI/D,qBAAa,MAAO,YAAW,aAAa;IAC1C,OAAO,CAAC,KAAK,CAA4B;IAEnC,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAW5C,GAAG,CACP,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,GAAG,EACV,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GACzB,OAAO,CAAC,OAAO,CAAC;IASb,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrC,QAAQ,CAAC,CAAC,EACd,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;KAAE,GACpD,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAgBd,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAI9C,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;CAK/B"}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FakeKV = void 0;
4
+ class FakeKV {
5
+ constructor() {
6
+ this.store = new Map();
7
+ }
8
+ async get(key) {
9
+ const now = Date.now();
10
+ const e = this.store.get(key);
11
+ if (!e)
12
+ return null;
13
+ if (e.expiresAt && e.expiresAt <= now) {
14
+ this.store.delete(key);
15
+ return null;
16
+ }
17
+ return e.value;
18
+ }
19
+ async set(key, value, opts) {
20
+ const entry = { value };
21
+ if (opts?.ttlSec && opts.ttlSec > 0) {
22
+ entry.expiresAt = Date.now() + opts.ttlSec * 1000;
23
+ }
24
+ this.store.set(key, entry);
25
+ return true;
26
+ }
27
+ async del(key) {
28
+ return this.store.delete(key);
29
+ }
30
+ async exists(key) {
31
+ const v = await this.get(key);
32
+ return v !== null && v !== undefined;
33
+ }
34
+ async withLock(key, fn, opts) {
35
+ // Simple, non-reentrant lock for testing; not robust
36
+ if (await this.exists(key)) {
37
+ if (opts?.onBusy === "skip")
38
+ return null;
39
+ throw new Error("lock busy");
40
+ }
41
+ // Respect requested TTL to better emulate production behavior
42
+ const ttl = opts?.ttlSec && opts.ttlSec > 0 ? opts.ttlSec : 1;
43
+ await this.set(key, { token: "lock" }, { ttlSec: ttl });
44
+ try {
45
+ return await fn();
46
+ }
47
+ finally {
48
+ await this.del(key);
49
+ }
50
+ }
51
+ async listKeys(_pattern) {
52
+ return Array.from(this.store.keys());
53
+ }
54
+ async clear() {
55
+ const n = this.store.size;
56
+ this.store.clear();
57
+ return n;
58
+ }
59
+ }
60
+ exports.FakeKV = FakeKV;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=management.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"management.test.d.ts","sourceRoot":"","sources":["../../src/test/management.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const management_1 = require("../management");
5
+ const fakeKV_1 = require("./helpers/fakeKV");
6
+ (0, vitest_1.describe)("management", () => {
7
+ const OLD_ENV = process.env;
8
+ (0, vitest_1.beforeEach)(() => {
9
+ process.env = { ...OLD_ENV };
10
+ process.env.PLUGIN_ADMIN_SECRET = "test";
11
+ process.env.STELLAR_NETWORK = "testnet";
12
+ process.env.SOROBAN_RPC_URL = "http://localhost:9999";
13
+ process.env.FUND_RELAYER_ID = "fund";
14
+ });
15
+ (0, vitest_1.afterEach)(() => {
16
+ process.env = OLD_ENV;
17
+ });
18
+ (0, vitest_1.test)("isManagementRequest detects shape", () => {
19
+ (0, vitest_1.expect)((0, management_1.isManagementRequest)({ management: {} })).toBe(true);
20
+ (0, vitest_1.expect)((0, management_1.isManagementRequest)({})).toBe(false);
21
+ });
22
+ (0, vitest_1.test)("list and set channel accounts", async () => {
23
+ const kv = new fakeKV_1.FakeKV();
24
+ const ctx = {
25
+ kv,
26
+ params: {
27
+ management: { adminSecret: "test", action: "listChannelAccounts" },
28
+ },
29
+ };
30
+ const list1 = await (0, management_1.handleManagement)(ctx);
31
+ (0, vitest_1.expect)(Array.isArray(list1.relayerIds)).toBe(true);
32
+ const ctxSet = {
33
+ kv,
34
+ params: {
35
+ management: {
36
+ adminSecret: "test",
37
+ action: "setChannelAccounts",
38
+ relayerIds: ["A", "B"],
39
+ },
40
+ },
41
+ };
42
+ const set = await (0, management_1.handleManagement)(ctxSet);
43
+ (0, vitest_1.expect)(set.ok).toBe(true);
44
+ (0, vitest_1.expect)(set.appliedRelayerIds).toEqual(["a", "b"]);
45
+ const list2 = await (0, management_1.handleManagement)(ctx);
46
+ (0, vitest_1.expect)(list2.relayerIds).toEqual(["a", "b"]);
47
+ });
48
+ (0, vitest_1.test)("locked conflict on removal", async () => {
49
+ const kv = new fakeKV_1.FakeKV();
50
+ // Seed list
51
+ await kv.set("testnet:channel:relayer-ids", { relayerIds: ["a", "b"] });
52
+ // Simulate lock on 'b'
53
+ await kv.set("testnet:channel:in-use:b", { token: "t" });
54
+ const ctx = {
55
+ kv,
56
+ params: {
57
+ management: {
58
+ adminSecret: "test",
59
+ action: "setChannelAccounts",
60
+ relayerIds: ["a"],
61
+ },
62
+ },
63
+ };
64
+ await (0, vitest_1.expect)((0, management_1.handleManagement)(ctx)).rejects.toThrow("Locked relayer IDs cannot be removed");
65
+ });
66
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pool.busy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.busy.test.d.ts","sourceRoot":"","sources":["../../src/test/pool.busy.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const pool_1 = require("../pool");
5
+ const fakeKV_1 = require("./helpers/fakeKV");
6
+ (0, vitest_1.describe)("ChannelPool busy mutex", () => {
7
+ (0, vitest_1.test)("acquire retries when global mutex is busy, then succeeds", async () => {
8
+ const kv = new fakeKV_1.FakeKV();
9
+ const pool = new pool_1.ChannelPool("testnet", kv);
10
+ // Configure a single relayer
11
+ await kv.set("testnet:channel:relayer-ids", { relayerIds: ["p1"] });
12
+ // Simulate busy global mutex briefly, then release
13
+ const globalKey = "testnet:channel-pool-lock";
14
+ await kv.set(globalKey, { token: "busy" });
15
+ setTimeout(() => {
16
+ void kv.del(globalKey);
17
+ }, 50);
18
+ const lock = await pool.acquire();
19
+ (0, vitest_1.expect)(lock.relayerId).toBe("p1");
20
+ // Release for cleanliness
21
+ await pool.release(lock);
22
+ });
23
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pool.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.test.d.ts","sourceRoot":"","sources":["../../src/test/pool.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const pool_1 = require("../pool");
5
+ const fakeKV_1 = require("./helpers/fakeKV");
6
+ (0, vitest_1.describe)("ChannelPool", () => {
7
+ (0, vitest_1.test)("acquire distributes and release removes lock", async () => {
8
+ const kv = new fakeKV_1.FakeKV();
9
+ const pool = new pool_1.ChannelPool("testnet", kv);
10
+ await kv.set("testnet:channel:relayer-ids", { relayerIds: ["p1", "p2"] });
11
+ const l1 = await pool.acquire();
12
+ const l2 = await pool.acquire();
13
+ (0, vitest_1.expect)(["p1", "p2"]).toContain(l1.relayerId);
14
+ (0, vitest_1.expect)(["p1", "p2"]).toContain(l2.relayerId);
15
+ (0, vitest_1.expect)(l1.relayerId).not.toEqual(l2.relayerId);
16
+ // Next acquire should fail (both locked)
17
+ await (0, vitest_1.expect)(pool.acquire()).rejects.toThrow("Too many transactions queued");
18
+ // Release one and ensure lock key gone
19
+ await pool.release(l1);
20
+ const stillLocked = await kv.exists(`testnet:channel:in-use:${l1.relayerId}`);
21
+ (0, vitest_1.expect)(stillLocked).toBe(false);
22
+ });
23
+ (0, vitest_1.test)("acquire fails on empty membership", async () => {
24
+ const kv = new fakeKV_1.FakeKV();
25
+ const pool = new pool_1.ChannelPool("testnet", kv);
26
+ await kv.set("testnet:channel:relayer-ids", { relayerIds: [] });
27
+ await (0, vitest_1.expect)(pool.acquire()).rejects.toThrow("No channel accounts configured");
28
+ });
29
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tx.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tx.test.d.ts","sourceRoot":"","sources":["../../src/test/tx.test.ts"],"names":[],"mappings":""}