@openzeppelin/relayer-plugin-channels 0.10.0 → 0.11.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/dist/plugin/config.d.ts +1 -0
- package/dist/plugin/config.d.ts.map +1 -1
- package/dist/plugin/config.js +8 -0
- package/dist/plugin/constants.d.ts +2 -0
- package/dist/plugin/constants.d.ts.map +1 -1
- package/dist/plugin/constants.js +2 -0
- package/dist/plugin/handler.d.ts +0 -2
- package/dist/plugin/handler.d.ts.map +1 -1
- package/dist/plugin/handler.js +51 -138
- package/dist/plugin/sequence.d.ts +30 -0
- package/dist/plugin/sequence.d.ts.map +1 -0
- package/dist/plugin/sequence.js +212 -0
- package/dist/plugin/simulation.d.ts.map +1 -1
- package/dist/plugin/simulation.js +29 -4
- package/dist/plugin/submit.d.ts +7 -4
- package/dist/plugin/submit.d.ts.map +1 -1
- package/dist/plugin/submit.js +48 -6
- package/package.json +1 -1
package/dist/plugin/config.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/plugin/config.js
CHANGED
|
@@ -91,6 +91,13 @@ function parseInclusionFee(envVar, defaultValue) {
|
|
|
91
91
|
return defaultValue;
|
|
92
92
|
return Math.floor(n);
|
|
93
93
|
}
|
|
94
|
+
function parseSequenceNumberCacheMaxAge() {
|
|
95
|
+
const raw = process.env.SEQUENCE_NUMBER_CACHE_MAX_AGE_MS;
|
|
96
|
+
if (!raw)
|
|
97
|
+
return constants_1.CONFIG.DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS;
|
|
98
|
+
const n = Number(raw);
|
|
99
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : constants_1.CONFIG.DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS;
|
|
100
|
+
}
|
|
94
101
|
function parseContractCapacityRatio() {
|
|
95
102
|
const raw = process.env.CONTRACT_CAPACITY_RATIO;
|
|
96
103
|
if (!raw)
|
|
@@ -124,6 +131,7 @@ function loadConfig() {
|
|
|
124
131
|
contractCapacityRatio: parseContractCapacityRatio(),
|
|
125
132
|
inclusionFeeDefault: parseInclusionFee('INCLUSION_FEE_DEFAULT', DEFAULT_INCLUSION_FEE_DEFAULT),
|
|
126
133
|
inclusionFeeLimited: parseInclusionFee('INCLUSION_FEE_LIMITED', DEFAULT_INCLUSION_FEE_LIMITED),
|
|
134
|
+
sequenceNumberCacheMaxAgeMs: parseSequenceNumberCacheMaxAge(),
|
|
127
135
|
};
|
|
128
136
|
}
|
|
129
137
|
/**
|
|
@@ -19,6 +19,7 @@ export declare const CONFIG: {
|
|
|
19
19
|
readonly MIN_LOCK_TTL_SECONDS: 3;
|
|
20
20
|
readonly MAX_LOCK_TTL_SECONDS: 30;
|
|
21
21
|
readonly DEFAULT_CONTRACT_CAPACITY_RATIO: 0.8;
|
|
22
|
+
readonly DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS: 120000;
|
|
22
23
|
};
|
|
23
24
|
export declare const POOL: {
|
|
24
25
|
readonly MUTEX_TTL_SECONDS: 1;
|
|
@@ -34,6 +35,7 @@ export declare const SIMULATION: {
|
|
|
34
35
|
readonly MIN_TIME_BOUND: 0;
|
|
35
36
|
readonly MAX_TIME_BOUND_OFFSET_SECONDS: 120;
|
|
36
37
|
readonly MAX_FUTURE_TIME_BOUND_SECONDS: 120;
|
|
38
|
+
readonly SIMULATION_AUTH_MODE: "enforce";
|
|
37
39
|
};
|
|
38
40
|
export declare const POLLING: {
|
|
39
41
|
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
|
|
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"}
|
package/dist/plugin/constants.js
CHANGED
|
@@ -24,6 +24,7 @@ exports.CONFIG = {
|
|
|
24
24
|
MIN_LOCK_TTL_SECONDS: 3,
|
|
25
25
|
MAX_LOCK_TTL_SECONDS: 30,
|
|
26
26
|
DEFAULT_CONTRACT_CAPACITY_RATIO: 0.8,
|
|
27
|
+
DEFAULT_SEQUENCE_NUMBER_CACHE_MAX_AGE_MS: 120000,
|
|
27
28
|
};
|
|
28
29
|
// Pool Constants
|
|
29
30
|
exports.POOL = {
|
|
@@ -44,6 +45,7 @@ exports.SIMULATION = {
|
|
|
44
45
|
MIN_TIME_BOUND: 0,
|
|
45
46
|
MAX_TIME_BOUND_OFFSET_SECONDS: 120,
|
|
46
47
|
MAX_FUTURE_TIME_BOUND_SECONDS: 120,
|
|
48
|
+
SIMULATION_AUTH_MODE: 'enforce',
|
|
47
49
|
};
|
|
48
50
|
// Polling for transactionWait
|
|
49
51
|
exports.POLLING = {
|
package/dist/plugin/handler.d.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Orchestrates the transaction processing pipeline using channel accounts with fee bumping.
|
|
6
6
|
*/
|
|
7
7
|
import { PluginContext } from '@openzeppelin/relayer-sdk';
|
|
8
|
-
import type { Relayer } from '@openzeppelin/relayer-sdk';
|
|
9
8
|
import { Transaction, xdr } from '@stellar/stellar-sdk';
|
|
10
9
|
/**
|
|
11
10
|
* Extracts func and auth from an unsigned Soroban transaction.
|
|
@@ -15,7 +14,6 @@ export declare function extractFuncAuthFromUnsignedXdr(tx: Transaction): {
|
|
|
15
14
|
func: xdr.HostFunction;
|
|
16
15
|
auth: xdr.SorobanAuthorizationEntry[];
|
|
17
16
|
} | null;
|
|
18
|
-
export declare function getAccountSequence(relayer: Relayer, address: string): Promise<string>;
|
|
19
17
|
/**
|
|
20
18
|
* Main plugin handler exported for OpenZeppelin Relayer
|
|
21
19
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/plugin/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/plugin/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAC;AASvE,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;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"}
|
package/dist/plugin/handler.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.extractFuncAuthFromUnsignedXdr = extractFuncAuthFromUnsignedXdr;
|
|
10
|
-
exports.getAccountSequence = getAccountSequence;
|
|
11
10
|
exports.handler = handler;
|
|
12
11
|
const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
|
|
13
12
|
const pool_1 = require("./pool");
|
|
@@ -21,6 +20,7 @@ const simulation_1 = require("./simulation");
|
|
|
21
20
|
const fee_1 = require("./fee");
|
|
22
21
|
const tx_1 = require("./tx");
|
|
23
22
|
const fee_tracking_1 = require("./fee-tracking");
|
|
23
|
+
const sequence_1 = require("./sequence");
|
|
24
24
|
function getApiKey(headers, headerName) {
|
|
25
25
|
const values = headers[headerName];
|
|
26
26
|
return values?.[0]?.trim() || undefined;
|
|
@@ -45,122 +45,8 @@ function extractFuncAuthFromUnsignedXdr(tx) {
|
|
|
45
45
|
auth: invokeHostFn.auth(),
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
async function
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
accountKey = stellar_sdk_1.xdr.LedgerKey.account(new stellar_sdk_1.xdr.LedgerKeyAccount({
|
|
52
|
-
accountId: stellar_sdk_1.Keypair.fromPublicKey(address).xdrPublicKey(),
|
|
53
|
-
}));
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
console.error('[channels] Sequence fetch failed', {
|
|
57
|
-
event: 'invalid_channel_account_address',
|
|
58
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
59
|
-
address,
|
|
60
|
-
message: error instanceof Error ? error.message : String(error),
|
|
61
|
-
});
|
|
62
|
-
throw (0, relayer_sdk_1.pluginError)('Invalid channel account address', {
|
|
63
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
64
|
-
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
65
|
-
details: { address, message: error instanceof Error ? error.message : String(error) },
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
let response;
|
|
69
|
-
try {
|
|
70
|
-
response = await relayer.rpc({
|
|
71
|
-
jsonrpc: '2.0',
|
|
72
|
-
id: Math.floor(Math.random() * 1e8).toString(),
|
|
73
|
-
method: 'getLedgerEntries',
|
|
74
|
-
params: {
|
|
75
|
-
keys: [accountKey.toXDR('base64')],
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
console.error('[channels] Sequence fetch failed', {
|
|
81
|
-
event: 'sequence_rpc_request_failed',
|
|
82
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
83
|
-
address,
|
|
84
|
-
message: error instanceof Error ? error.message : String(error),
|
|
85
|
-
});
|
|
86
|
-
throw (0, relayer_sdk_1.pluginError)('Failed to get sequence from channel relayer', {
|
|
87
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
88
|
-
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
89
|
-
details: { message: error instanceof Error ? error.message : String(error) },
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
if (response.error) {
|
|
93
|
-
console.error('[channels] Sequence fetch failed', {
|
|
94
|
-
event: 'sequence_rpc_error_response',
|
|
95
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
96
|
-
address,
|
|
97
|
-
message: response.error.message,
|
|
98
|
-
});
|
|
99
|
-
throw (0, relayer_sdk_1.pluginError)('Failed to get sequence from channel relayer', {
|
|
100
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
101
|
-
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
102
|
-
details: { message: response.error.message },
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
const result = response.result;
|
|
106
|
-
const entries = result?.entries;
|
|
107
|
-
if (!Array.isArray(entries)) {
|
|
108
|
-
console.error('[channels] Sequence fetch failed', {
|
|
109
|
-
event: 'sequence_rpc_invalid_result_shape',
|
|
110
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
111
|
-
address,
|
|
112
|
-
});
|
|
113
|
-
throw (0, relayer_sdk_1.pluginError)('Invalid RPC response for account sequence', {
|
|
114
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
115
|
-
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
116
|
-
details: { address },
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
if (!entries || entries.length === 0) {
|
|
120
|
-
console.warn('[channels] Sequence fetch returned no account entries', {
|
|
121
|
-
event: 'sequence_account_not_found',
|
|
122
|
-
code: 'ACCOUNT_NOT_FOUND',
|
|
123
|
-
address,
|
|
124
|
-
});
|
|
125
|
-
throw (0, relayer_sdk_1.pluginError)('Channel account not found on ledger', {
|
|
126
|
-
code: 'ACCOUNT_NOT_FOUND',
|
|
127
|
-
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
128
|
-
details: { address },
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
const firstEntryXdr = entries[0]?.xdr;
|
|
132
|
-
if (typeof firstEntryXdr !== 'string') {
|
|
133
|
-
console.error('[channels] Sequence fetch failed', {
|
|
134
|
-
event: 'sequence_rpc_invalid_entry_xdr',
|
|
135
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
136
|
-
address,
|
|
137
|
-
});
|
|
138
|
-
throw (0, relayer_sdk_1.pluginError)('Invalid RPC response for account sequence', {
|
|
139
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
140
|
-
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
141
|
-
details: { address },
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
try {
|
|
145
|
-
const accountEntry = stellar_sdk_1.xdr.LedgerEntryData.fromXDR(firstEntryXdr, 'base64');
|
|
146
|
-
return accountEntry.account().seqNum().toString();
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
console.error('[channels] Sequence fetch failed', {
|
|
150
|
-
event: 'sequence_xdr_decode_failed',
|
|
151
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
152
|
-
address,
|
|
153
|
-
message: error instanceof Error ? error.message : String(error),
|
|
154
|
-
});
|
|
155
|
-
throw (0, relayer_sdk_1.pluginError)('Failed to decode account sequence from ledger entry', {
|
|
156
|
-
code: 'FAILED_TO_GET_SEQUENCE',
|
|
157
|
-
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
158
|
-
details: { address, message: error instanceof Error ? error.message : String(error) },
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
async function handleXdrSubmit(xdrStr, fundRelayer, fundAddress, network, networkPassphrase, api, pool, acquireOptions, fees, tracker) {
|
|
163
|
-
const tx = new stellar_sdk_1.Transaction(xdrStr, networkPassphrase);
|
|
48
|
+
async function handleXdrSubmit(xdrStr, ctx) {
|
|
49
|
+
const tx = new stellar_sdk_1.Transaction(xdrStr, ctx.networkPassphrase);
|
|
164
50
|
// Unsigned XDR: extract func+auth and route through channel path
|
|
165
51
|
if (tx.signatures.length === 0) {
|
|
166
52
|
const extracted = extractFuncAuthFromUnsignedXdr(tx);
|
|
@@ -177,22 +63,22 @@ async function handleXdrSubmit(xdrStr, fundRelayer, fundAddress, network, networ
|
|
|
177
63
|
console.log(`[channels] Unsigned XDR detected, extracting func+auth and routing through channel path`);
|
|
178
64
|
// Update acquireOptions with contractId from extracted func
|
|
179
65
|
const contractId = (0, fee_1.getContractIdFromFunc)(extracted.func);
|
|
180
|
-
const updatedOptions = { ...acquireOptions, contractId };
|
|
181
|
-
return handleFuncAuthSubmit(extracted.func, extracted.auth,
|
|
66
|
+
const updatedOptions = { ...ctx.acquireOptions, contractId };
|
|
67
|
+
return handleFuncAuthSubmit(extracted.func, extracted.auth, { ...ctx, acquireOptions: updatedOptions });
|
|
182
68
|
}
|
|
183
69
|
const validated = (0, tx_1.validateExistingTransactionForSubmitOnly)(tx);
|
|
184
|
-
const maxFee = (0, fee_1.calculateMaxFee)(validated, acquireOptions.limitedContracts, fees);
|
|
70
|
+
const maxFee = (0, fee_1.calculateMaxFee)(validated, ctx.acquireOptions.limitedContracts, ctx.fees);
|
|
185
71
|
const contractId = (0, fee_1.getContractIdFromTransaction)(validated);
|
|
186
|
-
await tracker?.checkBudget(maxFee);
|
|
72
|
+
await ctx.tracker?.checkBudget(maxFee);
|
|
187
73
|
const submitContext = {
|
|
188
74
|
contractId,
|
|
189
|
-
isLimited: contractId ? acquireOptions.limitedContracts.has(contractId) : false,
|
|
75
|
+
isLimited: contractId ? ctx.acquireOptions.limitedContracts.has(contractId) : false,
|
|
190
76
|
};
|
|
191
|
-
return (0, submit_1.submitWithFeeBumpAndWait)(fundRelayer, validated.toXDR(), network, maxFee, api, tracker, submitContext);
|
|
77
|
+
return (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, validated.toXDR(), ctx.network, maxFee, ctx.api, ctx.tracker, submitContext);
|
|
192
78
|
}
|
|
193
|
-
async function handleFuncAuthSubmit(func, auth,
|
|
79
|
+
async function handleFuncAuthSubmit(func, auth, ctx) {
|
|
194
80
|
// Simulate once — used for both read-only detection and transaction assembly
|
|
195
|
-
const simulation = await (0, simulation_1.simulateTransaction)(func, auth, fundAddress, fundRelayer, networkPassphrase);
|
|
81
|
+
const simulation = await (0, simulation_1.simulateTransaction)(func, auth, ctx.fundAddress, ctx.fundRelayer, ctx.networkPassphrase);
|
|
196
82
|
if (simulation.isReadOnly) {
|
|
197
83
|
console.log(`[channels] Read-only call detected, returning simulation result`);
|
|
198
84
|
return {
|
|
@@ -205,8 +91,8 @@ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddr
|
|
|
205
91
|
}
|
|
206
92
|
let poolLock;
|
|
207
93
|
try {
|
|
208
|
-
poolLock = await pool.acquire(acquireOptions);
|
|
209
|
-
const channelRelayer = api.useRelayer(poolLock.relayerId);
|
|
94
|
+
poolLock = await ctx.pool.acquire(ctx.acquireOptions);
|
|
95
|
+
const channelRelayer = ctx.api.useRelayer(poolLock.relayerId);
|
|
210
96
|
const channelInfo = await channelRelayer.getRelayer();
|
|
211
97
|
console.log(`[channels] Acquired channel: ${poolLock.relayerId}`);
|
|
212
98
|
if (!channelInfo || !channelInfo.address) {
|
|
@@ -223,22 +109,35 @@ async function handleFuncAuthSubmit(func, auth, api, pool, fundRelayer, fundAddr
|
|
|
223
109
|
details: { network_type: channelInfo.network_type, relayerId: poolLock.relayerId },
|
|
224
110
|
});
|
|
225
111
|
}
|
|
226
|
-
const sequence = await
|
|
112
|
+
const sequence = await (0, sequence_1.getSequence)(ctx.kv, ctx.network, channelRelayer, channelInfo.address, ctx.sequenceNumberCacheMaxAgeMs);
|
|
227
113
|
// Assemble the transaction using the cached simulation result — no second RPC call
|
|
228
|
-
const built = (0, simulation_1.buildWithChannel)(func, auth, { address: channelInfo.address, sequence }, networkPassphrase, simulation.rawSimResult);
|
|
229
|
-
const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, fundRelayer, channelInfo.address, fundAddress, networkPassphrase);
|
|
230
|
-
const maxFee = (0, fee_1.calculateMaxFee)(signedTx, acquireOptions.limitedContracts, fees);
|
|
114
|
+
const built = (0, simulation_1.buildWithChannel)(func, auth, { address: channelInfo.address, sequence }, ctx.networkPassphrase, simulation.rawSimResult);
|
|
115
|
+
const signedTx = await (0, submit_1.signWithChannelAndFund)(built, channelRelayer, ctx.fundRelayer, channelInfo.address, ctx.fundAddress, ctx.networkPassphrase);
|
|
116
|
+
const maxFee = (0, fee_1.calculateMaxFee)(signedTx, ctx.acquireOptions.limitedContracts, ctx.fees);
|
|
231
117
|
const contractId = (0, fee_1.getContractIdFromFunc)(func);
|
|
232
|
-
await tracker?.checkBudget(maxFee);
|
|
118
|
+
await ctx.tracker?.checkBudget(maxFee);
|
|
233
119
|
const submitContext = {
|
|
234
120
|
contractId,
|
|
235
|
-
isLimited: contractId ? acquireOptions.limitedContracts.has(contractId) : false,
|
|
121
|
+
isLimited: contractId ? ctx.acquireOptions.limitedContracts.has(contractId) : false,
|
|
236
122
|
};
|
|
237
|
-
|
|
123
|
+
try {
|
|
124
|
+
const result = await (0, submit_1.submitWithFeeBumpAndWait)(ctx.fundRelayer, signedTx.toXDR(), ctx.network, maxFee, ctx.api, ctx.tracker, submitContext);
|
|
125
|
+
if (result.status === 'confirmed') {
|
|
126
|
+
await (0, sequence_1.commitSequence)(ctx.kv, ctx.network, channelInfo.address, sequence);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
await (0, sequence_1.clearSequence)(ctx.kv, ctx.network, channelInfo.address);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
238
137
|
}
|
|
239
138
|
finally {
|
|
240
139
|
if (poolLock) {
|
|
241
|
-
await pool.release(poolLock);
|
|
140
|
+
await ctx.pool.release(poolLock);
|
|
242
141
|
}
|
|
243
142
|
}
|
|
244
143
|
}
|
|
@@ -301,16 +200,30 @@ async function channelAccounts(context) {
|
|
|
301
200
|
inclusionFeeDefault: config.inclusionFeeDefault,
|
|
302
201
|
inclusionFeeLimited: config.inclusionFeeLimited,
|
|
303
202
|
};
|
|
304
|
-
// 4.
|
|
203
|
+
// 4. Build pipeline context
|
|
204
|
+
const ctx = {
|
|
205
|
+
api,
|
|
206
|
+
kv,
|
|
207
|
+
pool,
|
|
208
|
+
fundRelayer: fundRelayer,
|
|
209
|
+
fundAddress: fundInfo.address,
|
|
210
|
+
network: config.network,
|
|
211
|
+
networkPassphrase,
|
|
212
|
+
acquireOptions,
|
|
213
|
+
fees,
|
|
214
|
+
tracker,
|
|
215
|
+
sequenceNumberCacheMaxAgeMs: config.sequenceNumberCacheMaxAgeMs,
|
|
216
|
+
};
|
|
217
|
+
// 5. Branch by request type
|
|
305
218
|
if (request.type === 'xdr') {
|
|
306
219
|
console.log(`[channels] Flow: XDR submit-only`);
|
|
307
|
-
return await handleXdrSubmit(request.xdr,
|
|
220
|
+
return await handleXdrSubmit(request.xdr, ctx);
|
|
308
221
|
}
|
|
309
222
|
// Extract contractId for func+auth flow
|
|
310
223
|
const contractId = (0, fee_1.getContractIdFromFunc)(request.func);
|
|
311
224
|
const funcAcquireOptions = { ...acquireOptions, contractId };
|
|
312
225
|
console.log(`[channels] Flow: func+auth with channel account`);
|
|
313
|
-
return await handleFuncAuthSubmit(request.func, request.auth,
|
|
226
|
+
return await handleFuncAuthSubmit(request.func, request.auth, { ...ctx, acquireOptions: funcAcquireOptions });
|
|
314
227
|
}
|
|
315
228
|
/**
|
|
316
229
|
* Main plugin handler exported for OpenZeppelin Relayer
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequence.ts
|
|
3
|
+
*
|
|
4
|
+
* KV-based sequence number cache for channel accounts.
|
|
5
|
+
* After a transaction is confirmed on-chain, the RPC `getLedgerEntries` can
|
|
6
|
+
* still return the old (pre-increment) sequence number due to read-after-write
|
|
7
|
+
* lag. Caching the next expected sequence in KV avoids `tx_bad_seq` errors.
|
|
8
|
+
*/
|
|
9
|
+
import type { PluginKVStore, Relayer } from '@openzeppelin/relayer-sdk';
|
|
10
|
+
/**
|
|
11
|
+
* Fetch the current sequence number for an account directly from chain
|
|
12
|
+
* via `getLedgerEntries` RPC. Throws on any RPC or decoding failure.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getAccountSequence(relayer: Relayer, address: string): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Get the current sequence number for a channel account.
|
|
17
|
+
* Reads from KV cache first; falls back to chain via `getAccountSequence`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getSequence(kv: PluginKVStore, network: string, relayer: Relayer, address: string, sequenceNumberCacheMaxAgeMs: number): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Store the next expected sequence number in KV after a transaction
|
|
22
|
+
* has been confirmed on-chain.
|
|
23
|
+
*/
|
|
24
|
+
export declare function commitSequence(kv: PluginKVStore, network: string, address: string, usedSequence: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Clear cached sequence — forces a re-fetch from chain on next request.
|
|
27
|
+
* Used when transaction outcome is uncertain (e.g. timeout).
|
|
28
|
+
*/
|
|
29
|
+
export declare function clearSequence(kv: PluginKVStore, network: string, address: string): Promise<void>;
|
|
30
|
+
//# sourceMappingURL=sequence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequence.d.ts","sourceRoot":"","sources":["../../src/plugin/sequence.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAwBxE;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsH3F;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,aAAa,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,2BAA2B,EAAE,MAAM,GAClC,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,aAAa,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWtG"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sequence.ts
|
|
4
|
+
*
|
|
5
|
+
* KV-based sequence number cache for channel accounts.
|
|
6
|
+
* After a transaction is confirmed on-chain, the RPC `getLedgerEntries` can
|
|
7
|
+
* still return the old (pre-increment) sequence number due to read-after-write
|
|
8
|
+
* lag. Caching the next expected sequence in KV avoids `tx_bad_seq` errors.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.getAccountSequence = getAccountSequence;
|
|
12
|
+
exports.getSequence = getSequence;
|
|
13
|
+
exports.commitSequence = commitSequence;
|
|
14
|
+
exports.clearSequence = clearSequence;
|
|
15
|
+
const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
|
|
16
|
+
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
17
|
+
const constants_1 = require("./constants");
|
|
18
|
+
function seqKey(network, address) {
|
|
19
|
+
return `${network}:channel:seq:${address}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validates that a sequence number string is a non-negative integer.
|
|
23
|
+
* Returns true for strings like "0", "42", "123456789012345".
|
|
24
|
+
*/
|
|
25
|
+
function isValidSequence(seq) {
|
|
26
|
+
return /^\d+$/.test(seq);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fetch the current sequence number for an account directly from chain
|
|
30
|
+
* via `getLedgerEntries` RPC. Throws on any RPC or decoding failure.
|
|
31
|
+
*/
|
|
32
|
+
async function getAccountSequence(relayer, address) {
|
|
33
|
+
let accountKey;
|
|
34
|
+
try {
|
|
35
|
+
accountKey = stellar_sdk_1.xdr.LedgerKey.account(new stellar_sdk_1.xdr.LedgerKeyAccount({
|
|
36
|
+
accountId: stellar_sdk_1.Keypair.fromPublicKey(address).xdrPublicKey(),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('[channels] Sequence fetch failed', {
|
|
41
|
+
event: 'invalid_channel_account_address',
|
|
42
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
43
|
+
address,
|
|
44
|
+
message: error instanceof Error ? error.message : String(error),
|
|
45
|
+
});
|
|
46
|
+
throw (0, relayer_sdk_1.pluginError)('Invalid channel account address', {
|
|
47
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
48
|
+
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
49
|
+
details: { address, message: error instanceof Error ? error.message : String(error) },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
let response;
|
|
53
|
+
try {
|
|
54
|
+
response = await relayer.rpc({
|
|
55
|
+
jsonrpc: '2.0',
|
|
56
|
+
id: Math.floor(Math.random() * 1e8).toString(),
|
|
57
|
+
method: 'getLedgerEntries',
|
|
58
|
+
params: {
|
|
59
|
+
keys: [accountKey.toXDR('base64')],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('[channels] Sequence fetch failed', {
|
|
65
|
+
event: 'sequence_rpc_request_failed',
|
|
66
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
67
|
+
address,
|
|
68
|
+
message: error instanceof Error ? error.message : String(error),
|
|
69
|
+
});
|
|
70
|
+
throw (0, relayer_sdk_1.pluginError)('Failed to get sequence from channel relayer', {
|
|
71
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
72
|
+
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
73
|
+
details: { message: error instanceof Error ? error.message : String(error) },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (response.error) {
|
|
77
|
+
console.error('[channels] Sequence fetch failed', {
|
|
78
|
+
event: 'sequence_rpc_error_response',
|
|
79
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
80
|
+
address,
|
|
81
|
+
message: response.error.message,
|
|
82
|
+
});
|
|
83
|
+
throw (0, relayer_sdk_1.pluginError)('Failed to get sequence from channel relayer', {
|
|
84
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
85
|
+
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
86
|
+
details: { message: response.error.message },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const result = response.result;
|
|
90
|
+
const entries = result?.entries;
|
|
91
|
+
if (!Array.isArray(entries)) {
|
|
92
|
+
console.error('[channels] Sequence fetch failed', {
|
|
93
|
+
event: 'sequence_rpc_invalid_result_shape',
|
|
94
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
95
|
+
address,
|
|
96
|
+
});
|
|
97
|
+
throw (0, relayer_sdk_1.pluginError)('Invalid RPC response for account sequence', {
|
|
98
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
99
|
+
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
100
|
+
details: { address },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (!entries || entries.length === 0) {
|
|
104
|
+
console.warn('[channels] Sequence fetch returned no account entries', {
|
|
105
|
+
event: 'sequence_account_not_found',
|
|
106
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
107
|
+
address,
|
|
108
|
+
});
|
|
109
|
+
throw (0, relayer_sdk_1.pluginError)('Channel account not found on ledger', {
|
|
110
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
111
|
+
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
112
|
+
details: { address },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const firstEntryXdr = entries[0]?.xdr;
|
|
116
|
+
if (typeof firstEntryXdr !== 'string') {
|
|
117
|
+
console.error('[channels] Sequence fetch failed', {
|
|
118
|
+
event: 'sequence_rpc_invalid_entry_xdr',
|
|
119
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
120
|
+
address,
|
|
121
|
+
});
|
|
122
|
+
throw (0, relayer_sdk_1.pluginError)('Invalid RPC response for account sequence', {
|
|
123
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
124
|
+
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
125
|
+
details: { address },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const accountEntry = stellar_sdk_1.xdr.LedgerEntryData.fromXDR(firstEntryXdr, 'base64');
|
|
130
|
+
return accountEntry.account().seqNum().toString();
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.error('[channels] Sequence fetch failed', {
|
|
134
|
+
event: 'sequence_xdr_decode_failed',
|
|
135
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
136
|
+
address,
|
|
137
|
+
message: error instanceof Error ? error.message : String(error),
|
|
138
|
+
});
|
|
139
|
+
throw (0, relayer_sdk_1.pluginError)('Failed to decode account sequence from ledger entry', {
|
|
140
|
+
code: 'FAILED_TO_GET_SEQUENCE',
|
|
141
|
+
status: constants_1.HTTP_STATUS.BAD_GATEWAY,
|
|
142
|
+
details: { address, message: error instanceof Error ? error.message : String(error) },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get the current sequence number for a channel account.
|
|
148
|
+
* Reads from KV cache first; falls back to chain via `getAccountSequence`.
|
|
149
|
+
*/
|
|
150
|
+
async function getSequence(kv, network, relayer, address, sequenceNumberCacheMaxAgeMs) {
|
|
151
|
+
const key = seqKey(network, address);
|
|
152
|
+
try {
|
|
153
|
+
const cached = await kv.get(key);
|
|
154
|
+
if (cached?.sequence) {
|
|
155
|
+
if (!isValidSequence(cached.sequence)) {
|
|
156
|
+
console.warn(`[channels] Sequence cache invalid: address=${address}, value=${cached.sequence}, refetching`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const age = Date.now() - (cached.storedAt ?? 0);
|
|
160
|
+
if (age < sequenceNumberCacheMaxAgeMs) {
|
|
161
|
+
console.debug(`[channels] Sequence cache hit: address=${address}, seq=${cached.sequence}, age=${age}ms`);
|
|
162
|
+
return cached.sequence;
|
|
163
|
+
}
|
|
164
|
+
console.debug(`[channels] Sequence cache stale: address=${address}, age=${age}ms, refetching`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
console.warn(`[channels] Failed to read sequence from KV, falling back to chain`, {
|
|
170
|
+
address,
|
|
171
|
+
key,
|
|
172
|
+
error: err instanceof Error ? err.message : String(err),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return getAccountSequence(relayer, address);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Store the next expected sequence number in KV after a transaction
|
|
179
|
+
* has been confirmed on-chain.
|
|
180
|
+
*/
|
|
181
|
+
async function commitSequence(kv, network, address, usedSequence) {
|
|
182
|
+
const next = (BigInt(usedSequence) + 1n).toString();
|
|
183
|
+
const key = seqKey(network, address);
|
|
184
|
+
try {
|
|
185
|
+
await kv.set(key, { sequence: next, storedAt: Date.now() });
|
|
186
|
+
console.debug(`[channels] Sequence committed: address=${address}, next=${next}`);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
console.warn(`[channels] Failed to commit sequence to KV`, {
|
|
190
|
+
address,
|
|
191
|
+
next,
|
|
192
|
+
error: err instanceof Error ? err.message : String(err),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Clear cached sequence — forces a re-fetch from chain on next request.
|
|
198
|
+
* Used when transaction outcome is uncertain (e.g. timeout).
|
|
199
|
+
*/
|
|
200
|
+
async function clearSequence(kv, network, address) {
|
|
201
|
+
const key = seqKey(network, address);
|
|
202
|
+
try {
|
|
203
|
+
await kv.del(key);
|
|
204
|
+
console.debug(`[channels] Sequence cleared: address=${address}`);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
console.warn(`[channels] Failed to clear sequence from KV`, {
|
|
208
|
+
address,
|
|
209
|
+
error: err instanceof Error ? err.message : String(err),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -41,7 +41,8 @@ async function simulateTransaction(func, auth, sourceAddress, relayer, networkPa
|
|
|
41
41
|
jsonrpc: '2.0',
|
|
42
42
|
id: Math.floor(Math.random() * 1e8).toString(),
|
|
43
43
|
method: 'simulateTransaction',
|
|
44
|
-
|
|
44
|
+
// Enforce mode validates auth entry signatures during simulation.
|
|
45
|
+
params: { transaction: transaction.toXDR(), authMode: constants_1.SIMULATION.SIMULATION_AUTH_MODE },
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
catch (err) {
|
|
@@ -65,11 +66,13 @@ async function simulateTransaction(func, auth, sourceAddress, relayer, networkPa
|
|
|
65
66
|
...rpcResponse.result,
|
|
66
67
|
};
|
|
67
68
|
if ('error' in simResult && simResult.error) {
|
|
69
|
+
const parsedError = parseSimulationError(simResult.error);
|
|
70
|
+
const failure = classifySimulationFailure(simResult.error, parsedError);
|
|
68
71
|
console.error(`[channels] Simulation error: ${simResult.error}`);
|
|
69
|
-
throw (0, relayer_sdk_1.pluginError)(
|
|
70
|
-
code:
|
|
72
|
+
throw (0, relayer_sdk_1.pluginError)(failure.message, {
|
|
73
|
+
code: failure.code,
|
|
71
74
|
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
72
|
-
details: { error:
|
|
75
|
+
details: { error: parsedError, authMode: constants_1.SIMULATION.SIMULATION_AUTH_MODE },
|
|
73
76
|
});
|
|
74
77
|
}
|
|
75
78
|
// Read-only detection
|
|
@@ -148,3 +151,25 @@ function parseSimulationError(error) {
|
|
|
148
151
|
}
|
|
149
152
|
return firstLine;
|
|
150
153
|
}
|
|
154
|
+
function classifySimulationFailure(rawError, parsedError) {
|
|
155
|
+
const isEnforcedAuthValidation = constants_1.SIMULATION.SIMULATION_AUTH_MODE === 'enforce' &&
|
|
156
|
+
(/\bError\(Auth,/i.test(rawError) ||
|
|
157
|
+
/\brequire_auth\b/i.test(rawError) ||
|
|
158
|
+
/\binvalid\s+signature\b/i.test(rawError) ||
|
|
159
|
+
/\bsignature\s+has\s+expired\b/i.test(rawError) ||
|
|
160
|
+
/\bsignature\s+expired\b/i.test(rawError) ||
|
|
161
|
+
/\bsignature\s+verification\s+failed\b/i.test(rawError) ||
|
|
162
|
+
/\bbad[_\s]?signature\b/i.test(rawError) ||
|
|
163
|
+
/\btx_bad_auth\b/i.test(rawError) ||
|
|
164
|
+
/\bbad[_\s]?auth\b/i.test(rawError));
|
|
165
|
+
if (isEnforcedAuthValidation) {
|
|
166
|
+
return {
|
|
167
|
+
code: 'SIMULATION_SIGNED_AUTH_VALIDATION_FAILED',
|
|
168
|
+
message: `Signed auth entry validation failed in enforce simulation: ${parsedError}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
code: 'SIMULATION_FAILED',
|
|
173
|
+
message: 'Simulation failed',
|
|
174
|
+
};
|
|
175
|
+
}
|
package/dist/plugin/submit.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface SubmitContext {
|
|
|
11
11
|
contractId?: string;
|
|
12
12
|
isLimited?: boolean;
|
|
13
13
|
}
|
|
14
|
+
interface DecodedTransactionResult {
|
|
15
|
+
feeCharged: number;
|
|
16
|
+
resultCode: string;
|
|
17
|
+
}
|
|
14
18
|
/**
|
|
15
19
|
* Sign transaction with both channel and fund relayers
|
|
16
20
|
* - First sign with channel account
|
|
@@ -23,10 +27,9 @@ export declare function signWithChannelAndFund(transaction: Transaction, channel
|
|
|
23
27
|
*/
|
|
24
28
|
export declare function submitWithFeeBumpAndWait(fundRelayer: Relayer, signedXdr: string, network: 'testnet' | 'mainnet', maxFee: number, api: PluginAPI, tracker?: FeeTracker, context?: SubmitContext): Promise<ChannelAccountsResponse>;
|
|
25
29
|
/** Try to decode a transaction result XDR from the reason string */
|
|
26
|
-
export declare function decodeTransactionResult(reason: string):
|
|
27
|
-
|
|
28
|
-
resultCode: string;
|
|
29
|
-
} | null;
|
|
30
|
+
export declare function decodeTransactionResult(reason: string): DecodedTransactionResult | null;
|
|
31
|
+
export declare function buildStellarLabTransactionUrl(network: 'testnet' | 'mainnet', txHash: string): string;
|
|
30
32
|
/** Strip provider wrapper text, extract last segment (e.g., "TxInsufficientBalance") */
|
|
31
33
|
export declare function sanitizeReason(reason: string): string;
|
|
34
|
+
export {};
|
|
32
35
|
//# sourceMappingURL=submit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submit.d.ts","sourceRoot":"","sources":["../../src/plugin/submit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAEL,OAAO,EAGP,SAAS,EACV,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;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,CAuBtB;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,EACd,OAAO,CAAC,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,uBAAuB,CAAC,
|
|
1
|
+
{"version":3,"file":"submit.d.ts","sourceRoot":"","sources":["../../src/plugin/submit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAO,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAEL,OAAO,EAGP,SAAS,EACV,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,wBAAwB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;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,CAuBtB;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,EACd,OAAO,CAAC,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,uBAAuB,CAAC,CAmFlC;AASD,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,wBAAwB,GAAG,IAAI,CAkCvF;AAeD,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAepG;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOrD"}
|
package/dist/plugin/submit.js
CHANGED
|
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
exports.signWithChannelAndFund = signWithChannelAndFund;
|
|
9
9
|
exports.submitWithFeeBumpAndWait = submitWithFeeBumpAndWait;
|
|
10
10
|
exports.decodeTransactionResult = decodeTransactionResult;
|
|
11
|
+
exports.buildStellarLabTransactionUrl = buildStellarLabTransactionUrl;
|
|
11
12
|
exports.sanitizeReason = sanitizeReason;
|
|
12
13
|
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
13
14
|
const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
|
|
@@ -66,10 +67,14 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
|
|
|
66
67
|
}
|
|
67
68
|
const rawReason = final.status_reason || 'Transaction failed';
|
|
68
69
|
const decoded = decodeTransactionResult(rawReason);
|
|
70
|
+
const labUrl = final.hash ? buildStellarLabTransactionUrl(network, final.hash) : null;
|
|
69
71
|
const contractType = context?.isLimited ? 'limited' : 'default';
|
|
70
72
|
const base = `[channels] Transaction failed: contractId=${context?.contractId ?? 'unknown'}, contractType=${contractType}, maxFee=${maxFee}`;
|
|
71
|
-
if (decoded
|
|
72
|
-
|
|
73
|
+
if (decoded && isTxInsufficientFeeError(decoded.resultCode)) {
|
|
74
|
+
const feeInfo = decoded.feeCharged != null
|
|
75
|
+
? `, requiredFee=${decoded.feeCharged}, shortfall=${decoded.feeCharged - maxFee}`
|
|
76
|
+
: '';
|
|
77
|
+
console.error(`${base}, reason=txInsufficientFee${feeInfo}`);
|
|
73
78
|
}
|
|
74
79
|
else if (decoded) {
|
|
75
80
|
console.error(`${base}, reason=${decoded.resultCode}`);
|
|
@@ -78,7 +83,8 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
|
|
|
78
83
|
console.error(`${base}, reason=${rawReason}`);
|
|
79
84
|
}
|
|
80
85
|
const reason = sanitizeReason(rawReason);
|
|
81
|
-
|
|
86
|
+
const reasonWithLab = labUrl ? `${reason}. Debug in Stellar Lab (click "Load Transaction"): ${labUrl}` : reason;
|
|
87
|
+
throw (0, relayer_sdk_1.pluginError)(reasonWithLab, {
|
|
82
88
|
code: 'ONCHAIN_FAILED',
|
|
83
89
|
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
84
90
|
details: {
|
|
@@ -86,6 +92,8 @@ async function submitWithFeeBumpAndWait(fundRelayer, signedXdr, network, maxFee,
|
|
|
86
92
|
reason,
|
|
87
93
|
id: final.id,
|
|
88
94
|
hash: final.hash ?? null,
|
|
95
|
+
resultCode: decoded?.resultCode ?? null,
|
|
96
|
+
labUrl: labUrl ? `Debug this failure in Stellar Lab (click "Load Transaction"): ${labUrl}` : null,
|
|
89
97
|
},
|
|
90
98
|
});
|
|
91
99
|
}
|
|
@@ -123,17 +131,26 @@ function isSignTransactionResponseStellar(data) {
|
|
|
123
131
|
}
|
|
124
132
|
/** Try to decode a transaction result XDR from the reason string */
|
|
125
133
|
function decodeTransactionResult(reason) {
|
|
134
|
+
const fromReasonText = extractResultCodeFromReasonText(reason);
|
|
135
|
+
if (fromReasonText) {
|
|
136
|
+
return {
|
|
137
|
+
feeCharged: 0,
|
|
138
|
+
resultCode: fromReasonText,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
126
141
|
try {
|
|
127
142
|
const match = reason.match(/([A-Za-z0-9+/=]{20,})$/);
|
|
128
143
|
if (!match)
|
|
129
144
|
return null;
|
|
130
145
|
const result = stellar_sdk_1.xdr.TransactionResult.fromXDR(match[1], 'base64');
|
|
131
|
-
|
|
146
|
+
const outerResultCode = String(result.result().switch().name);
|
|
147
|
+
let resultCode = outerResultCode;
|
|
132
148
|
// Unwrap fee bump inner failure to get the actual result code
|
|
133
|
-
if (
|
|
149
|
+
if (outerResultCode === 'txFeeBumpInnerFailed') {
|
|
134
150
|
try {
|
|
135
151
|
const innerResult = result.result().innerResultPair().result();
|
|
136
|
-
|
|
152
|
+
const innerResultCode = String(innerResult.result().switch().name);
|
|
153
|
+
resultCode = `${outerResultCode}:${innerResultCode}`;
|
|
137
154
|
}
|
|
138
155
|
catch {
|
|
139
156
|
// keep outer result code if unwrap fails
|
|
@@ -148,6 +165,31 @@ function decodeTransactionResult(reason) {
|
|
|
148
165
|
return null;
|
|
149
166
|
}
|
|
150
167
|
}
|
|
168
|
+
function extractResultCodeFromReasonText(reason) {
|
|
169
|
+
const outerMatch = reason.match(/\bSpecific XDR reason:\s*([A-Za-z0-9_]+)\b/i);
|
|
170
|
+
if (!outerMatch?.[1])
|
|
171
|
+
return null;
|
|
172
|
+
const innerMatch = reason.match(/\bInner result:\s*([A-Za-z0-9_]+)\b/i);
|
|
173
|
+
return innerMatch?.[1] ? `${outerMatch[1]}:${innerMatch[1]}` : outerMatch[1];
|
|
174
|
+
}
|
|
175
|
+
/** Check if the result code indicates an insufficient fee error (case-insensitive). */
|
|
176
|
+
function isTxInsufficientFeeError(resultCode) {
|
|
177
|
+
return resultCode?.toLowerCase() === 'txinsufficientfee';
|
|
178
|
+
}
|
|
179
|
+
function buildStellarLabTransactionUrl(network, txHash) {
|
|
180
|
+
const isMainnet = network === 'mainnet';
|
|
181
|
+
const networkId = isMainnet ? 'mainnet' : 'testnet';
|
|
182
|
+
const label = isMainnet ? 'Mainnet' : 'Testnet';
|
|
183
|
+
const horizonUrl = isMainnet ? 'https://horizon.stellar.org' : 'https://horizon-testnet.stellar.org';
|
|
184
|
+
const rpcUrl = isMainnet ? 'https://mainnet.sorobanrpc.com' : 'https://soroban-testnet.stellar.org';
|
|
185
|
+
const passphrase = isMainnet ? 'Public Global Stellar Network ; September 2015' : 'Test SDF Network ; September 2015';
|
|
186
|
+
// Stellar Lab expects protocol values encoded as https://// in query params.
|
|
187
|
+
// txHash is intentionally left unencoded because it is a hex string.
|
|
188
|
+
const horizonParam = horizonUrl.replace('https://', 'https:////');
|
|
189
|
+
const rpcParam = rpcUrl.replace('https://', 'https:////');
|
|
190
|
+
const passphraseParam = passphrase.replace(/ /g, '%20').replace(/;/g, '%3B');
|
|
191
|
+
return `https://lab.stellar.org/transaction/dashboard?$=network$id=${networkId}&label=${label}&horizonUrl=${horizonParam}&rpcUrl=${rpcParam}&passphrase=${passphraseParam}&txDashboard$transactionHash=${txHash};;`;
|
|
192
|
+
}
|
|
151
193
|
/** Strip provider wrapper text, extract last segment (e.g., "TxInsufficientBalance") */
|
|
152
194
|
function sanitizeReason(reason) {
|
|
153
195
|
const segments = reason.split(/:\s*/);
|