@openzeppelin/relayer-plugin-channels 0.11.0 → 0.12.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 CHANGED
@@ -233,6 +233,12 @@ export CONTRACT_CAPACITY_RATIO=0.8 # Max ratio of pool for limited contra
233
233
  # Inclusion fee overrides (optional)
234
234
  export INCLUSION_FEE_DEFAULT=203 # Inclusion fee in stroops for regular contracts (default: BASE_FEE * 2 + 3 = 203)
235
235
  export INCLUSION_FEE_LIMITED=201 # Inclusion fee in stroops for limited contracts (default: BASE_FEE * 2 + 1 = 201)
236
+
237
+ # Sequence number cache (optional)
238
+ export SEQUENCE_NUMBER_CACHE_MAX_AGE_MS=120000 # Max age of cached sequence numbers in ms (default: 120000)
239
+
240
+ # Auth expiry validation (optional)
241
+ export MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER=2 # Minimum ledger margin for auth entry signatureExpirationLedger (default: 2)
236
242
  ```
237
243
 
238
244
  Your Relayer should now contain:
@@ -940,25 +946,67 @@ Plugin error example:
940
946
  - **Key**: `<network>:api-key-limit:<apiKey>`
941
947
  - **Value**: `{ limit: number }` (custom fee limit in stroops)
942
948
 
949
+ ### Sequence Number Cache
950
+
951
+ - **Key**: `<network>:channel:seq:<address>`
952
+ - **Value**: `{ sequence: string, storedAt: number }` (sequence number and cache timestamp in ms)
953
+
943
954
  ## Error Codes
944
955
 
956
+ ### Configuration
957
+
945
958
  - `CONFIG_MISSING`: Missing required environment variable
959
+ - `CONFIG_INVALID`: Invalid configuration value (e.g., invalid contract address in `LIMITED_CONTRACTS`)
946
960
  - `UNSUPPORTED_NETWORK`: Invalid network type
961
+
962
+ ### Request Validation
963
+
947
964
  - `INVALID_PARAMS`: Invalid request parameters
948
965
  - `INVALID_XDR`: Failed to parse XDR
949
966
  - `INVALID_ENVELOPE_TYPE`: Not a regular transaction envelope
967
+ - `INVALID_UNSIGNED_XDR`: Unsigned XDR must contain exactly one `invokeHostFunction` operation
950
968
  - `INVALID_TIME_BOUNDS`: TimeBounds too far in the future
969
+ - `TIMEBOUNDS_EXPIRED`: Transaction timebounds have expired
970
+ - `TIMEBOUNDS_TOO_FAR`: Transaction timebounds maxTime too far in the future
971
+ - `FEE_MISMATCH`: Fee mismatch in signed transaction
972
+ - `INVALID_OPERATION_SOURCE`: Operation source does not match transaction source
973
+
974
+ ### Pool & Channel
975
+
951
976
  - `NO_CHANNELS_CONFIGURED`: No channel accounts have been configured via management API
952
977
  - `POOL_CAPACITY`: All channel accounts in use
953
978
  - `RELAYER_UNAVAILABLE`: Relayer not found
979
+ - `FAILED_TO_GET_SEQUENCE`: Failed to fetch channel account sequence number
980
+ - `ACCOUNT_NOT_FOUND`: Channel account not found on the ledger
981
+
982
+ ### Simulation & Assembly
983
+
954
984
  - `SIMULATION_FAILED`: Transaction simulation failed
985
+ - `SIMULATION_NETWORK_ERROR`: Simulation network request failed (HTTP 502)
986
+ - `SIMULATION_RPC_FAILURE`: Simulation RPC provider error (HTTP 502)
987
+ - `SIMULATION_SIGNED_AUTH_VALIDATION_FAILED`: Signed auth entry validation failed in enforce-mode simulation
988
+ - `AUTH_EXPIRY_TOO_SHORT`: Auth entry `signatureExpirationLedger` too close to current ledger
989
+ - `ASSEMBLY_FAILED`: Transaction assembly failed
990
+
991
+ ### Submission
992
+
993
+ - `INVALID_SIGNATURE`: Invalid channel signature response
955
994
  - `ONCHAIN_FAILED`: Transaction failed on-chain
956
995
  - `WAIT_TIMEOUT`: Transaction wait timeout
996
+
997
+ ### Fee Tracking
998
+
999
+ - `API_KEY_REQUIRED`: API key header missing when `FEE_LIMIT` is configured
1000
+ - `FEE_LIMIT_EXCEEDED`: API key has exceeded its fee limit (HTTP 429)
1001
+
1002
+ ### Management
1003
+
957
1004
  - `MANAGEMENT_DISABLED`: Management API not enabled
958
1005
  - `UNAUTHORIZED`: Invalid admin secret
1006
+ - `INVALID_ACTION`: Invalid management action
1007
+ - `INVALID_PAYLOAD`: Invalid management request payload
959
1008
  - `LOCKED_CONFLICT`: Cannot remove locked channel accounts
960
- - `API_KEY_REQUIRED`: API key header missing when `FEE_LIMIT` is configured (HTTP 403)
961
- - `FEE_LIMIT_EXCEEDED`: API key has exceeded its fee limit (HTTP 429)
1009
+ - `KV_ERROR`: KV store operation failed
962
1010
 
963
1011
  ## License
964
1012
 
@@ -16,6 +16,7 @@ export interface ChannelAccountsConfig {
16
16
  inclusionFeeDefault: number;
17
17
  inclusionFeeLimited: number;
18
18
  sequenceNumberCacheMaxAgeMs: number;
19
+ minSignatureExpirationLedgerBuffer: number;
19
20
  }
20
21
  /**
21
22
  * Load configuration from environment variables
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/plugin/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,2BAA2B,EAAE,MAAM,CAAC;CACrC;AAmGD;;GAEG;AACH,wBAAgB,UAAU,IAAI,qBAAqB,CAuBlD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAE3E"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/plugin/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,2BAA2B,EAAE,MAAM,CAAC;IACpC,kCAAkC,EAAE,MAAM,CAAC;CAC5C;AA0GD;;GAEG;AACH,wBAAgB,UAAU,IAAI,qBAAqB,CAwBlD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAE3E"}
@@ -91,6 +91,13 @@ function parseInclusionFee(envVar, defaultValue) {
91
91
  return defaultValue;
92
92
  return Math.floor(n);
93
93
  }
94
+ function parseMinAuthExpiryLedgerBuffer() {
95
+ const raw = process.env.MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER;
96
+ if (!raw)
97
+ return constants_1.CONFIG.DEFAULT_MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER;
98
+ const n = Number(raw);
99
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : constants_1.CONFIG.DEFAULT_MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER;
100
+ }
94
101
  function parseSequenceNumberCacheMaxAge() {
95
102
  const raw = process.env.SEQUENCE_NUMBER_CACHE_MAX_AGE_MS;
96
103
  if (!raw)
@@ -132,6 +139,7 @@ function loadConfig() {
132
139
  inclusionFeeDefault: parseInclusionFee('INCLUSION_FEE_DEFAULT', DEFAULT_INCLUSION_FEE_DEFAULT),
133
140
  inclusionFeeLimited: parseInclusionFee('INCLUSION_FEE_LIMITED', DEFAULT_INCLUSION_FEE_LIMITED),
134
141
  sequenceNumberCacheMaxAgeMs: parseSequenceNumberCacheMaxAge(),
142
+ minSignatureExpirationLedgerBuffer: parseMinAuthExpiryLedgerBuffer(),
135
143
  };
136
144
  }
137
145
  /**
@@ -20,6 +20,7 @@ export declare const CONFIG: {
20
20
  readonly MAX_LOCK_TTL_SECONDS: 30;
21
21
  readonly DEFAULT_CONTRACT_CAPACITY_RATIO: 0.8;
22
22
  readonly DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS: 120000;
23
+ readonly DEFAULT_MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2;
23
24
  };
24
25
  export declare const POOL: {
25
26
  readonly MUTEX_TTL_SECONDS: 1;
@@ -36,6 +37,8 @@ export declare const SIMULATION: {
36
37
  readonly MAX_TIME_BOUND_OFFSET_SECONDS: 120;
37
38
  readonly MAX_FUTURE_TIME_BOUND_SECONDS: 120;
38
39
  readonly SIMULATION_AUTH_MODE: "enforce";
40
+ /** Minimum ledger margin required between latestLedger and auth signatureExpirationLedger. Must be > 1 since simulation already validates 1 ledger of validity. ~10s at ~5s/ledger. */
41
+ readonly MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2;
39
42
  };
40
43
  export declare const POLLING: {
41
44
  readonly INTERVAL_MS: 1000;
@@ -1 +1 @@
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;;;;;;CAMT,CAAC;AAGX,eAAO,MAAM,IAAI;;;;;CAOP,CAAC;AAGX,eAAO,MAAM,IAAI;;CAEP,CAAC;AAGX,eAAO,MAAM,UAAU;;;;;;CAMb,CAAC;AAGX,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAEX,eAAO,MAAM,GAAG;;CAGN,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/plugin/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,WAAW;;;;;;;;;;CAUd,CAAC;AAGX,eAAO,MAAM,MAAM;;;;;;;CAOT,CAAC;AAGX,eAAO,MAAM,IAAI;;;;;CAOP,CAAC;AAGX,eAAO,MAAM,IAAI;;CAEP,CAAC;AAGX,eAAO,MAAM,UAAU;;;;;;IAMrB,uLAAuL;;CAE/K,CAAC;AAGX,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAEX,eAAO,MAAM,GAAG;;CAGN,CAAC"}
@@ -25,6 +25,7 @@ exports.CONFIG = {
25
25
  MAX_LOCK_TTL_SECONDS: 30,
26
26
  DEFAULT_CONTRACT_CAPACITY_RATIO: 0.8,
27
27
  DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS: 120000,
28
+ DEFAULT_MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2,
28
29
  };
29
30
  // Pool Constants
30
31
  exports.POOL = {
@@ -46,6 +47,8 @@ exports.SIMULATION = {
46
47
  MAX_TIME_BOUND_OFFSET_SECONDS: 120,
47
48
  MAX_FUTURE_TIME_BOUND_SECONDS: 120,
48
49
  SIMULATION_AUTH_MODE: 'enforce',
50
+ /** Minimum ledger margin required between latestLedger and auth signatureExpirationLedger. Must be > 1 since simulation already validates 1 ledger of validity. ~10s at ~5s/ledger. */
51
+ MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER: 2,
49
52
  };
50
53
  // Polling for transactionWait
51
54
  exports.POLLING = {
@@ -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;AASvE,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AA0BxD;;;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;AAqPD;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAElE"}
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;AA0BxD;;;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;AAsPD;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAElE"}
@@ -109,9 +109,9 @@ async function handleFuncAuthSubmit(func, auth, ctx) {
109
109
  details: { network_type: channelInfo.network_type, relayerId: poolLock.relayerId },
110
110
  });
111
111
  }
112
- const sequence = await (0, sequence_1.getSequence)(ctx.kv, ctx.network, channelRelayer, channelInfo.address, ctx.sequenceNumberCacheMaxAgeMs);
112
+ const sequence = await (0, sequence_1.getSequence)(ctx.kv, ctx.network, channelRelayer, channelInfo.address, ctx.config.sequenceNumberCacheMaxAgeMs);
113
113
  // Assemble the transaction using the cached simulation result — no second RPC call
114
- const built = (0, simulation_1.buildWithChannel)(func, auth, { address: channelInfo.address, sequence }, ctx.networkPassphrase, simulation.rawSimResult);
114
+ const built = (0, simulation_1.buildWithChannel)(func, auth, { address: channelInfo.address, sequence }, ctx.networkPassphrase, simulation.rawSimResult, ctx.config.minSignatureExpirationLedgerBuffer);
115
115
  const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, ctx.fundRelayer, channelInfo.address, ctx.fundAddress, ctx.networkPassphrase);
116
116
  const maxFee = (0, fee_1.calculateMaxFee)(signedTx, ctx.acquireOptions.limitedContracts, ctx.fees);
117
117
  const contractId = (0, fee_1.getContractIdFromFunc)(func);
@@ -212,7 +212,7 @@ async function channelAccounts(context) {
212
212
  acquireOptions,
213
213
  fees,
214
214
  tracker,
215
- sequenceNumberCacheMaxAgeMs: config.sequenceNumberCacheMaxAgeMs,
215
+ config,
216
216
  };
217
217
  // 5. Branch by request type
218
218
  if (request.type === 'xdr') {
@@ -39,7 +39,7 @@ export declare function simulateTransaction(func: xdr.HostFunction, auth: xdr.So
39
39
  * Build and assemble a transaction using a channel account and an already-
40
40
  * obtained simulation result. No additional network calls are made.
41
41
  */
42
- export declare function buildWithChannel(func: xdr.HostFunction, auth: xdr.SorobanAuthorizationEntry[] | undefined, channel: ChannelAccount, networkPassphrase: string, simResult: rpc.Api.RawSimulateTransactionResponse): Transaction;
42
+ export declare function buildWithChannel(func: xdr.HostFunction, auth: xdr.SorobanAuthorizationEntry[] | undefined, channel: ChannelAccount, networkPassphrase: string, simResult: rpc.Api.RawSimulateTransactionResponse, minSignatureExpirationLedgerBuffer?: number): Transaction;
43
43
  /** Extract human-readable message + error type from simulation error diagnostic events */
44
44
  export declare function parseSimulationError(error: string): string;
45
45
  //# sourceMappingURL=simulation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"simulation.d.ts","sourceRoot":"","sources":["../../src/plugin/simulation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAsB,GAAG,EAAE,WAAW,EAAsB,GAAG,EAAE,MAAM,sBAAsB,CAAC;AACrG,OAAO,EAAgD,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAGlG,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,UAAU,EAAE,OAAO,CAAC;IACpB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,YAAY,EAAE,GAAG,CAAC,GAAG,CAAC,8BAA8B,CAAC;CACtD;AAOD;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,gBAAgB,CAAC,CAiF3B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,OAAO,EAAE,cAAc,EACvB,iBAAiB,EAAE,MAAM,EACzB,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,8BAA8B,GAChD,WAAW,CAsCb;AACD,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAe1D"}
1
+ {"version":3,"file":"simulation.d.ts","sourceRoot":"","sources":["../../src/plugin/simulation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAsB,GAAG,EAAE,WAAW,EAAsB,GAAG,EAAE,MAAM,sBAAsB,CAAC;AACrG,OAAO,EAAgD,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAGlG,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,UAAU,EAAE,OAAO,CAAC;IACpB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,YAAY,EAAE,GAAG,CAAC,GAAG,CAAC,8BAA8B,CAAC;CACtD;AAOD;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,gBAAgB,CAAC,CAiF3B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,GAAG,CAAC,YAAY,EACtB,IAAI,EAAE,GAAG,CAAC,yBAAyB,EAAE,GAAG,SAAS,EACjD,OAAO,EAAE,cAAc,EACvB,iBAAiB,EAAE,MAAM,EACzB,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,8BAA8B,EACjD,kCAAkC,GAAE,MAA0D,GAC7F,WAAW,CA4Cb;AA4CD,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAe1D"}
@@ -104,7 +104,12 @@ async function simulateTransaction(func, auth, sourceAddress, relayer, networkPa
104
104
  * Build and assemble a transaction using a channel account and an already-
105
105
  * obtained simulation result. No additional network calls are made.
106
106
  */
107
- function buildWithChannel(func, auth, channel, networkPassphrase, simResult) {
107
+ function buildWithChannel(func, auth, channel, networkPassphrase, simResult, minSignatureExpirationLedgerBuffer = constants_1.SIMULATION.MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER) {
108
+ // Fail fast: reject auth entries whose signatureExpirationLedger is too close
109
+ // to latestLedger before doing any assembly work.
110
+ if (simResult.latestLedger) {
111
+ validateAuthExpiry(auth, simResult.latestLedger, minSignatureExpirationLedgerBuffer);
112
+ }
108
113
  const now = Math.floor(Date.now() / 1000);
109
114
  console.debug(`[channels] Building tx: channel=${channel.address}, seq=${channel.sequence}, auth_count=${auth?.length ?? 0}`);
110
115
  // Build inner transaction (source = channel)
@@ -137,6 +142,36 @@ function buildWithChannel(func, auth, channel, networkPassphrase, simResult) {
137
142
  });
138
143
  }
139
144
  }
145
+ /**
146
+ * Reject transactions where any address-credentialed auth entry has a
147
+ * signatureExpirationLedger too close to the current ledger. This catches
148
+ * tight expiries early instead of failing on-chain with "signature has expired".
149
+ */
150
+ function validateAuthExpiry(authEntries, latestLedger, minBuffer) {
151
+ if (!authEntries?.length)
152
+ return;
153
+ for (const entry of authEntries) {
154
+ const creds = entry.credentials();
155
+ if (creds.switch() !== stellar_sdk_1.xdr.SorobanCredentialsType.sorobanCredentialsAddress()) {
156
+ continue;
157
+ }
158
+ const expiry = creds.address().signatureExpirationLedger();
159
+ const margin = expiry - latestLedger;
160
+ if (margin < minBuffer) {
161
+ console.error(`[channels] Auth entry signatureExpirationLedger too close to current ledger (expires in ${margin} ledgers, minimum ${minBuffer} required)`);
162
+ throw (0, relayer_sdk_1.pluginError)(`Auth entry signatureExpirationLedger too close to current ledger (expires in ${margin} ledgers, minimum ${minBuffer} required)`, {
163
+ code: 'AUTH_EXPIRY_TOO_SHORT',
164
+ status: constants_1.HTTP_STATUS.BAD_REQUEST,
165
+ details: {
166
+ signatureExpirationLedger: expiry,
167
+ latestLedger,
168
+ margin,
169
+ minimumRequired: minBuffer,
170
+ },
171
+ });
172
+ }
173
+ }
174
+ }
140
175
  /** Extract human-readable message + error type from simulation error diagnostic events */
141
176
  function parseSimulationError(error) {
142
177
  const firstLine = error.split('\n')[0]?.trim() || 'Simulation failed';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/relayer-plugin-channels",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "OpenZeppelin Relayer Plugin for Stellar Channel Accounts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",