@pushchain/core 4.0.14-alpha.0 → 5.1.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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/lib/constants/abi/cea.evm.d.ts +23 -0
- package/src/lib/constants/abi/cea.evm.js +34 -0
- package/src/lib/constants/abi/cea.evm.js.map +1 -0
- package/src/lib/constants/abi/ceaFactory.evm.d.ts +65 -0
- package/src/lib/constants/abi/ceaFactory.evm.js +41 -0
- package/src/lib/constants/abi/ceaFactory.evm.js.map +1 -0
- package/src/lib/constants/abi/erc20.evm.d.ts +14 -0
- package/src/lib/constants/abi/erc20.evm.js +7 -0
- package/src/lib/constants/abi/erc20.evm.js.map +1 -1
- package/src/lib/constants/abi/index.d.ts +6 -0
- package/src/lib/constants/abi/index.js +15 -1
- package/src/lib/constants/abi/index.js.map +1 -1
- package/src/lib/constants/abi/prc20.evm.d.ts +188 -0
- package/src/lib/constants/abi/prc20.evm.js +130 -0
- package/src/lib/constants/abi/prc20.evm.js.map +1 -0
- package/src/lib/constants/abi/uea-factory.d.ts +30 -0
- package/src/lib/constants/abi/uea-factory.js +25 -0
- package/src/lib/constants/abi/uea-factory.js.map +1 -0
- package/src/lib/constants/abi/universalGateway.evm.d.ts +93 -0
- package/src/lib/constants/abi/universalGateway.evm.js +70 -0
- package/src/lib/constants/abi/universalGateway.evm.js.map +1 -0
- package/src/lib/constants/abi/universalGatewayPC.evm.d.ts +140 -0
- package/src/lib/constants/abi/universalGatewayPC.evm.js +70 -0
- package/src/lib/constants/abi/universalGatewayPC.evm.js.map +1 -0
- package/src/lib/constants/abi/universalGatewayV0.evm.js +57 -0
- package/src/lib/constants/abi/universalGatewayV0.evm.js.map +1 -1
- package/src/lib/constants/abi/universalGatewayV0.json +1162 -1647
- package/src/lib/constants/chain.d.ts +52 -0
- package/src/lib/constants/chain.js +161 -1
- package/src/lib/constants/chain.js.map +1 -1
- package/src/lib/constants/index.d.ts +9 -1
- package/src/lib/constants/index.js +20 -1
- package/src/lib/constants/index.js.map +1 -1
- package/src/lib/constants/selectors.d.ts +42 -0
- package/src/lib/constants/selectors.js +45 -0
- package/src/lib/constants/selectors.js.map +1 -0
- package/src/lib/constants/tokens.d.ts +41 -0
- package/src/lib/constants/tokens.js +62 -1
- package/src/lib/constants/tokens.js.map +1 -1
- package/src/lib/generated/uexecutor/v2/index.d.ts +2 -0
- package/src/lib/generated/uexecutor/v2/index.js +31 -0
- package/src/lib/generated/uexecutor/v2/index.js.map +1 -0
- package/src/lib/generated/uexecutor/v2/query.d.ts +23 -0
- package/src/lib/generated/uexecutor/v2/query.js +79 -0
- package/src/lib/generated/uexecutor/v2/query.js.map +1 -0
- package/src/lib/generated/uexecutor/v2/types.d.ts +101 -0
- package/src/lib/generated/uexecutor/v2/types.js +660 -0
- package/src/lib/generated/uexecutor/v2/types.js.map +1 -0
- package/src/lib/generated/v1/tx.d.ts +22 -0
- package/src/lib/generated/v1/tx.js +188 -1
- package/src/lib/generated/v1/tx.js.map +1 -1
- package/src/lib/index.d.ts +5 -0
- package/src/lib/index.js +25 -1
- package/src/lib/index.js.map +1 -1
- package/src/lib/orchestrator/cea-utils.d.ts +85 -0
- package/src/lib/orchestrator/cea-utils.js +186 -0
- package/src/lib/orchestrator/cea-utils.js.map +1 -0
- package/src/lib/orchestrator/orchestrator.d.ts +326 -3
- package/src/lib/orchestrator/orchestrator.js +3262 -135
- package/src/lib/orchestrator/orchestrator.js.map +1 -1
- package/src/lib/orchestrator/orchestrator.types.d.ts +487 -0
- package/src/lib/orchestrator/orchestrator.types.js +17 -0
- package/src/lib/orchestrator/orchestrator.types.js.map +1 -1
- package/src/lib/orchestrator/payload-builders.d.ts +210 -1
- package/src/lib/orchestrator/payload-builders.js +481 -0
- package/src/lib/orchestrator/payload-builders.js.map +1 -1
- package/src/lib/orchestrator/route-detector.d.ts +102 -0
- package/src/lib/orchestrator/route-detector.js +355 -0
- package/src/lib/orchestrator/route-detector.js.map +1 -0
- package/src/lib/price-fetch/price-fetch.js +5 -2
- package/src/lib/price-fetch/price-fetch.js.map +1 -1
- package/src/lib/progress-hook/progress-hook.js +43 -0
- package/src/lib/progress-hook/progress-hook.js.map +1 -1
- package/src/lib/progress-hook/progress-hook.types.d.ts +7 -1
- package/src/lib/progress-hook/progress-hook.types.js +7 -0
- package/src/lib/progress-hook/progress-hook.types.js.map +1 -1
- package/src/lib/push-chain/push-chain.d.ts +71 -1
- package/src/lib/push-chain/push-chain.js +70 -1
- package/src/lib/push-chain/push-chain.js.map +1 -1
- package/src/lib/push-client/push-client.d.ts +10 -1
- package/src/lib/push-client/push-client.js +35 -6
- package/src/lib/push-client/push-client.js.map +1 -1
- package/src/lib/vm-client/evm-client.d.ts +3 -1
- package/src/lib/vm-client/evm-client.js +23 -15
- package/src/lib/vm-client/evm-client.js.map +1 -1
|
@@ -8,8 +8,12 @@ const viem_1 = require("viem");
|
|
|
8
8
|
const abi_1 = require("../constants/abi");
|
|
9
9
|
const uea_evm_1 = require("../constants/abi/uea.evm");
|
|
10
10
|
const chain_1 = require("../constants/chain");
|
|
11
|
+
const uea_factory_1 = require("../constants/abi/uea-factory");
|
|
11
12
|
const enums_1 = require("../constants/enums");
|
|
12
13
|
const tokens_1 = require("../constants/tokens");
|
|
14
|
+
const types_1 = require("../generated/uexecutor/v1/types");
|
|
15
|
+
const types_2 = require("../generated/uexecutor/v2/types");
|
|
16
|
+
const orchestrator_types_1 = require("./orchestrator.types");
|
|
13
17
|
const tx_1 = require("../generated/v1/tx");
|
|
14
18
|
const price_fetch_1 = require("../price-fetch/price-fetch");
|
|
15
19
|
const progress_hook_1 = tslib_1.__importDefault(require("../progress-hook/progress-hook"));
|
|
@@ -20,6 +24,9 @@ const push_client_1 = require("../push-client/push-client");
|
|
|
20
24
|
const evm_client_1 = require("../vm-client/evm-client");
|
|
21
25
|
const svm_client_1 = require("../vm-client/svm-client");
|
|
22
26
|
const payload_builders_1 = require("./payload-builders");
|
|
27
|
+
const route_detector_1 = require("./route-detector");
|
|
28
|
+
const cea_utils_1 = require("./cea-utils");
|
|
29
|
+
const selectors_1 = require("../constants/selectors");
|
|
23
30
|
class Orchestrator {
|
|
24
31
|
constructor(universalSigner, pushNetwork, rpcUrls = {}, printTraces = false, progressHook) {
|
|
25
32
|
this.universalSigner = universalSigner;
|
|
@@ -27,6 +34,13 @@ class Orchestrator {
|
|
|
27
34
|
this.rpcUrls = rpcUrls;
|
|
28
35
|
this.printTraces = printTraces;
|
|
29
36
|
this.progressHook = progressHook;
|
|
37
|
+
this.accountStatusCache = null;
|
|
38
|
+
/**
|
|
39
|
+
* Per-chain cache for detected gateway version (v0 or v1).
|
|
40
|
+
* Populated on first successful gateway tx per chain via try-V1-then-V0 fallback.
|
|
41
|
+
* TODO: Remove this cache + fallback logic once all chains are upgraded to V1.
|
|
42
|
+
*/
|
|
43
|
+
this.gatewayVersionCache = new Map();
|
|
30
44
|
let pushChain;
|
|
31
45
|
if (pushNetwork === enums_1.PUSH_NETWORK.MAINNET) {
|
|
32
46
|
pushChain = enums_1.CHAIN.PUSH_MAINNET;
|
|
@@ -43,6 +57,9 @@ class Orchestrator {
|
|
|
43
57
|
rpcUrls: pushChainRPCs,
|
|
44
58
|
network: pushNetwork,
|
|
45
59
|
});
|
|
60
|
+
// BNB is already upgraded to V1 — pre-seed the cache so it never falls back to V0.
|
|
61
|
+
// TODO: Remove these entries once all chains are upgraded to V1.
|
|
62
|
+
this.gatewayVersionCache.set(enums_1.CHAIN.BNB_TESTNET, 'v1');
|
|
46
63
|
}
|
|
47
64
|
/**
|
|
48
65
|
* Read-only accessors for current Orchestrator configuration
|
|
@@ -59,12 +76,242 @@ class Orchestrator {
|
|
|
59
76
|
getProgressHook() {
|
|
60
77
|
return this.progressHook;
|
|
61
78
|
}
|
|
79
|
+
// =========================================================================
|
|
80
|
+
// Account Status & UEA Upgrade
|
|
81
|
+
// =========================================================================
|
|
62
82
|
/**
|
|
63
|
-
*
|
|
83
|
+
* Fetches account status including UEA deployment state and version info.
|
|
84
|
+
* Results are cached — pass forceRefresh to bypass cache.
|
|
64
85
|
*/
|
|
65
|
-
|
|
86
|
+
getAccountStatus(options) {
|
|
66
87
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
67
|
-
|
|
88
|
+
if (this.accountStatusCache && !(options === null || options === void 0 ? void 0 : options.forceRefresh)) {
|
|
89
|
+
return this.accountStatusCache;
|
|
90
|
+
}
|
|
91
|
+
const chain = this.universalSigner.account.chain;
|
|
92
|
+
const { vm } = chain_1.CHAIN_INFO[chain];
|
|
93
|
+
const isReadOnly = false; // This is called from Orchestrator which always has a signer
|
|
94
|
+
const { deployed } = yield this.getUeaStatusAndNonce();
|
|
95
|
+
if (!deployed) {
|
|
96
|
+
const status = {
|
|
97
|
+
mode: 'signer',
|
|
98
|
+
uea: {
|
|
99
|
+
loaded: true,
|
|
100
|
+
deployed: false,
|
|
101
|
+
version: '',
|
|
102
|
+
minRequiredVersion: '',
|
|
103
|
+
requiresUpgrade: false,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
this.accountStatusCache = status;
|
|
107
|
+
return status;
|
|
108
|
+
}
|
|
109
|
+
// Fetch current version and latest version from factory in parallel
|
|
110
|
+
const [currentVersion, minRequiredVersion] = yield Promise.all([
|
|
111
|
+
this.fetchUEAVersion(),
|
|
112
|
+
this.fetchLatestUEAVersion(vm),
|
|
113
|
+
]);
|
|
114
|
+
const requiresUpgrade = (0, orchestrator_types_1.parseUEAVersion)(currentVersion) < (0, orchestrator_types_1.parseUEAVersion)(minRequiredVersion);
|
|
115
|
+
const status = {
|
|
116
|
+
mode: 'signer',
|
|
117
|
+
uea: {
|
|
118
|
+
loaded: true,
|
|
119
|
+
deployed: true,
|
|
120
|
+
version: currentVersion,
|
|
121
|
+
minRequiredVersion,
|
|
122
|
+
requiresUpgrade,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
this.accountStatusCache = status;
|
|
126
|
+
return status;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Upgrades the UEA to the latest implementation version.
|
|
131
|
+
* Sends MsgMigrateUEA Cosmos message with EIP-712 signed MigrationPayload.
|
|
132
|
+
* The UEA contract delegates to the UEAMigration contract to update implementation.
|
|
133
|
+
*/
|
|
134
|
+
upgradeAccount(options) {
|
|
135
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
136
|
+
const hook = (options === null || options === void 0 ? void 0 : options.progressHook) || this.progressHook;
|
|
137
|
+
const fireHook = (hookId, ...args) => {
|
|
138
|
+
const hookEntry = progress_hook_1.default[hookId];
|
|
139
|
+
if (hookEntry && hook) {
|
|
140
|
+
hook(hookEntry(...args));
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
// Step 1: Check status
|
|
144
|
+
fireHook(progress_hook_types_1.PROGRESS_HOOK.UEA_MIG_01);
|
|
145
|
+
const status = yield this.getAccountStatus({ forceRefresh: true });
|
|
146
|
+
if (!status.uea.requiresUpgrade) {
|
|
147
|
+
fireHook(progress_hook_types_1.PROGRESS_HOOK.UEA_MIG_9903);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Step 2: Awaiting signature
|
|
151
|
+
fireHook(progress_hook_types_1.PROGRESS_HOOK.UEA_MIG_02);
|
|
152
|
+
try {
|
|
153
|
+
const { chain, address } = this.universalSigner.account;
|
|
154
|
+
const { vm, chainId } = chain_1.CHAIN_INFO[chain];
|
|
155
|
+
const ueaAddress = this.computeUEAOffchain();
|
|
156
|
+
const migrationContractAddress = chain_1.UEA_MIGRATION[this.pushNetwork];
|
|
157
|
+
if (!migrationContractAddress || migrationContractAddress === '0xTBD') {
|
|
158
|
+
throw new Error('UEA migration contract address not configured');
|
|
159
|
+
}
|
|
160
|
+
// Read UEA nonce
|
|
161
|
+
const { nonce } = yield this.getUeaStatusAndNonce();
|
|
162
|
+
const deadline = BigInt(9999999999);
|
|
163
|
+
const ueaVersion = status.uea.version || '0.1.0';
|
|
164
|
+
// Sign MigrationPayload via EIP-712
|
|
165
|
+
if (!this.universalSigner.signTypedData) {
|
|
166
|
+
throw new Error('signTypedData not available on signer');
|
|
167
|
+
}
|
|
168
|
+
const signatureBytes = yield this.universalSigner.signTypedData({
|
|
169
|
+
domain: {
|
|
170
|
+
version: ueaVersion,
|
|
171
|
+
chainId: Number(chainId),
|
|
172
|
+
verifyingContract: ueaAddress,
|
|
173
|
+
},
|
|
174
|
+
types: {
|
|
175
|
+
MigrationPayload: [
|
|
176
|
+
{ name: 'migration', type: 'address' },
|
|
177
|
+
{ name: 'nonce', type: 'uint256' },
|
|
178
|
+
{ name: 'deadline', type: 'uint256' },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
primaryType: 'MigrationPayload',
|
|
182
|
+
message: {
|
|
183
|
+
migration: migrationContractAddress,
|
|
184
|
+
nonce,
|
|
185
|
+
deadline,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const signature = (0, viem_1.bytesToHex)(signatureBytes);
|
|
189
|
+
// Build Cosmos message
|
|
190
|
+
fireHook(progress_hook_types_1.PROGRESS_HOOK.UEA_MIG_03);
|
|
191
|
+
const universalAccountId = {
|
|
192
|
+
chainNamespace: chain_1.VM_NAMESPACE[vm],
|
|
193
|
+
chainId,
|
|
194
|
+
owner: vm === enums_1.VM.EVM
|
|
195
|
+
? address
|
|
196
|
+
: vm === enums_1.VM.SVM
|
|
197
|
+
? (0, viem_1.bytesToHex)(new Uint8Array(anchor_1.utils.bytes.bs58.decode(address)))
|
|
198
|
+
: address,
|
|
199
|
+
};
|
|
200
|
+
const { cosmosAddress: signer } = this.pushClient.getSignerAddress();
|
|
201
|
+
const msg = this.pushClient.createMsgMigrateUEA({
|
|
202
|
+
signer,
|
|
203
|
+
universalAccountId,
|
|
204
|
+
migrationPayload: {
|
|
205
|
+
migration: migrationContractAddress,
|
|
206
|
+
nonce: nonce.toString(),
|
|
207
|
+
deadline: deadline.toString(),
|
|
208
|
+
},
|
|
209
|
+
signature,
|
|
210
|
+
});
|
|
211
|
+
const txBody = yield this.pushClient.createCosmosTxBody([msg]);
|
|
212
|
+
const txRaw = yield this.pushClient.signCosmosTx(txBody);
|
|
213
|
+
const tx = yield this.pushClient.broadcastCosmosTx(txRaw);
|
|
214
|
+
if (tx.code !== 0) {
|
|
215
|
+
throw new Error(tx.rawLog || 'UEA migration transaction failed');
|
|
216
|
+
}
|
|
217
|
+
// Clear version cache so next read picks up new version
|
|
218
|
+
this.ueaVersionCache = undefined;
|
|
219
|
+
// Refresh account status
|
|
220
|
+
const updated = yield this.getAccountStatus({ forceRefresh: true });
|
|
221
|
+
fireHook(progress_hook_types_1.PROGRESS_HOOK.UEA_MIG_9901, updated.uea.version);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
fireHook(progress_hook_types_1.PROGRESS_HOOK.UEA_MIG_9902);
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Fetches the latest UEA version from UEAFactory contract.
|
|
231
|
+
* This is the version of the newest registered implementation — used as minRequiredVersion.
|
|
232
|
+
* Returns empty string if factory is unavailable.
|
|
233
|
+
*/
|
|
234
|
+
fetchLatestUEAVersion(vm) {
|
|
235
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
236
|
+
const factoryAddress = chain_1.UEA_FACTORY[this.pushNetwork];
|
|
237
|
+
if (!factoryAddress || factoryAddress === '0xTBD') {
|
|
238
|
+
return '';
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const vmHash = vm === enums_1.VM.EVM
|
|
242
|
+
? (0, viem_1.keccak256)((0, viem_1.encodeAbiParameters)([{ type: 'string' }], ['EVM']))
|
|
243
|
+
: (0, viem_1.keccak256)((0, viem_1.encodeAbiParameters)([{ type: 'string' }], ['SVM']));
|
|
244
|
+
return yield this.pushClient.readContract({
|
|
245
|
+
address: factoryAddress,
|
|
246
|
+
abi: uea_factory_1.UEA_FACTORY_ABI,
|
|
247
|
+
functionName: 'UEA_VERSION',
|
|
248
|
+
args: [vmHash],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (_a) {
|
|
252
|
+
return '';
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Migrate the CEA contract on an external chain to the latest version.
|
|
258
|
+
* Sends a MIGRATION_SELECTOR payload via Route 2 to trigger CEA upgrade.
|
|
259
|
+
*
|
|
260
|
+
* @param chain - The external chain where the CEA should be migrated
|
|
261
|
+
* @returns Transaction response
|
|
262
|
+
*/
|
|
263
|
+
migrateCEA(chain) {
|
|
264
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
265
|
+
var _a;
|
|
266
|
+
if (this.isPushChain(chain)) {
|
|
267
|
+
throw new Error('Cannot migrate CEA on Push Chain');
|
|
268
|
+
}
|
|
269
|
+
if (!(0, cea_utils_1.chainSupportsCEA)(chain)) {
|
|
270
|
+
throw new Error(`Chain ${chain} does not support CEA`);
|
|
271
|
+
}
|
|
272
|
+
const ueaAddress = this.computeUEAOffchain();
|
|
273
|
+
const { cea, isDeployed } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, chain, (_a = this.rpcUrls[chain]) === null || _a === void 0 ? void 0 : _a[0]);
|
|
274
|
+
if (!isDeployed) {
|
|
275
|
+
throw new Error(`CEA not deployed on chain ${chain}. Deploy CEA first.`);
|
|
276
|
+
}
|
|
277
|
+
return this.execute({
|
|
278
|
+
to: { address: cea, chain },
|
|
279
|
+
migration: true,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Executes a transaction with automatic route detection.
|
|
285
|
+
*
|
|
286
|
+
* Supports both simple Push Chain transactions and multi-chain routing:
|
|
287
|
+
* - Route 1 (UOA_TO_PUSH): `to` is a simple address string
|
|
288
|
+
* - Route 2 (UOA_TO_CEA): `to` is `{ address, chain }` targeting external chain
|
|
289
|
+
* - Route 3 (CEA_TO_PUSH): `from.chain` specified, targeting Push Chain
|
|
290
|
+
* - Route 4 (CEA_TO_CEA): `from.chain` specified, targeting external chain
|
|
291
|
+
*
|
|
292
|
+
* @param params - ExecuteParams or UniversalExecuteParams
|
|
293
|
+
* @returns Transaction response
|
|
294
|
+
*/
|
|
295
|
+
execute(params) {
|
|
296
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
297
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
|
|
298
|
+
// Lazy UEA upgrade check
|
|
299
|
+
if (!this.accountStatusCache || !this.accountStatusCache.uea.loaded) {
|
|
300
|
+
yield this.getAccountStatus();
|
|
301
|
+
}
|
|
302
|
+
if (((_a = this.accountStatusCache) === null || _a === void 0 ? void 0 : _a.uea.deployed) &&
|
|
303
|
+
((_b = this.accountStatusCache) === null || _b === void 0 ? void 0 : _b.uea.requiresUpgrade)) {
|
|
304
|
+
yield this.upgradeAccount({ progressHook: this.progressHook });
|
|
305
|
+
}
|
|
306
|
+
// Check if this is a multi-chain request (has ChainTarget or from.chain)
|
|
307
|
+
const isMultiChain = (0, route_detector_1.isChainTarget)(params.to) || ('from' in params && ((_c = params.from) === null || _c === void 0 ? void 0 : _c.chain));
|
|
308
|
+
if (isMultiChain) {
|
|
309
|
+
// sendTransaction delegates to prepareTransaction().send() for multi-chain routes
|
|
310
|
+
const prepared = yield this.prepareTransaction(params);
|
|
311
|
+
return prepared.send();
|
|
312
|
+
}
|
|
313
|
+
// Standard Push Chain execution (Route 1)
|
|
314
|
+
const execute = params;
|
|
68
315
|
// Create buffer to collect events during execution for tx.progressHook() replay
|
|
69
316
|
const eventBuffer = [];
|
|
70
317
|
// Store original progressHook and wrap to collect events
|
|
@@ -186,14 +433,7 @@ class Orchestrator {
|
|
|
186
433
|
isNative, bridgeAmount: bridgeAmount.toString(),
|
|
187
434
|
nativeAmount: nativeAmount.toString(), bridgeToken,
|
|
188
435
|
}, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2));
|
|
189
|
-
txHash = yield evmClient.
|
|
190
|
-
abi: abi_1.UNIVERSAL_GATEWAY_V0,
|
|
191
|
-
address: gatewayAddress,
|
|
192
|
-
functionName: 'sendUniversalTx',
|
|
193
|
-
args: [req],
|
|
194
|
-
signer: this.universalSigner,
|
|
195
|
-
value: isNative ? nativeAmount + bridgeAmount : nativeAmount,
|
|
196
|
-
});
|
|
436
|
+
txHash = yield this.sendGatewayTxWithFallback(evmClient, gatewayAddress, req, this.universalSigner, isNative ? nativeAmount + bridgeAmount : nativeAmount);
|
|
197
437
|
}
|
|
198
438
|
else {
|
|
199
439
|
// FUNDS ONLY OTHER
|
|
@@ -214,14 +454,7 @@ class Orchestrator {
|
|
|
214
454
|
isNative, bridgeAmount: bridgeAmount.toString(),
|
|
215
455
|
nativeAmount: nativeAmount.toString(), bridgeToken,
|
|
216
456
|
}, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2));
|
|
217
|
-
txHash = yield evmClient.
|
|
218
|
-
abi: abi_1.UNIVERSAL_GATEWAY_V0,
|
|
219
|
-
address: gatewayAddress,
|
|
220
|
-
functionName: 'sendUniversalTx',
|
|
221
|
-
args: [req],
|
|
222
|
-
signer: this.universalSigner,
|
|
223
|
-
value: isNative ? nativeAmount + bridgeAmount : nativeAmount,
|
|
224
|
-
});
|
|
457
|
+
txHash = yield this.sendGatewayTxWithFallback(evmClient, gatewayAddress, req, this.universalSigner, isNative ? nativeAmount + bridgeAmount : nativeAmount);
|
|
225
458
|
}
|
|
226
459
|
}
|
|
227
460
|
catch (err) {
|
|
@@ -240,14 +473,14 @@ class Orchestrator {
|
|
|
240
473
|
evmGatewayMethod: execute.to === ueaAddress ? 'sendFunds' : 'sendTxWithFunds',
|
|
241
474
|
}));
|
|
242
475
|
const pushChainUniversalTx = yield this.queryUniversalTxStatusFromGatewayTx(evmClient, gatewayAddress, txHash, execute.to === ueaAddress ? 'sendFunds' : 'sendTxWithFunds');
|
|
243
|
-
if (!((
|
|
476
|
+
if (!((_d = pushChainUniversalTx === null || pushChainUniversalTx === void 0 ? void 0 : pushChainUniversalTx.pcTx) === null || _d === void 0 ? void 0 : _d.length)) {
|
|
244
477
|
throw new Error(`Failed to retrieve Push Chain transaction status for gateway tx: ${txHash}. ` +
|
|
245
478
|
`The transaction may have failed on Push Chain or not been indexed yet.`);
|
|
246
479
|
}
|
|
247
480
|
// For sendFunds operations, MintPC (first) succeeds and executePayload (second) may fail
|
|
248
481
|
// Always use the last pcTx entry as it represents the final execution result
|
|
249
482
|
const lastPcTransaction = pushChainUniversalTx.pcTx.at(-1);
|
|
250
|
-
this.printLog('sendFunds — pushChainUniversalTx pcTx: ' + JSON.stringify((
|
|
483
|
+
this.printLog('sendFunds — pushChainUniversalTx pcTx: ' + JSON.stringify((_e = pushChainUniversalTx === null || pushChainUniversalTx === void 0 ? void 0 : pushChainUniversalTx.pcTx) === null || _e === void 0 ? void 0 : _e.map((p) => ({ txHash: p.txHash, status: p.status, errorMsg: p.errorMsg })), null, 2));
|
|
251
484
|
this.printLog('sendFunds — using lastPcTransaction: ' + JSON.stringify(lastPcTransaction, null, 2));
|
|
252
485
|
if (!(lastPcTransaction === null || lastPcTransaction === void 0 ? void 0 : lastPcTransaction.txHash)) {
|
|
253
486
|
// Check for error messages in failed entries
|
|
@@ -270,7 +503,7 @@ class Orchestrator {
|
|
|
270
503
|
const programId = new web3_js_1.PublicKey(abi_1.SVM_GATEWAY_IDL.address);
|
|
271
504
|
const [configPda] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('config')], programId);
|
|
272
505
|
const [vaultPda] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('vault')], programId);
|
|
273
|
-
const
|
|
506
|
+
const { feeVaultPda, protocolFeeLamports } = yield this._getSvmProtocolFee(svmClient, programId);
|
|
274
507
|
const [rateLimitConfigPda] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('rate_limit_config')], programId);
|
|
275
508
|
const userPk = new web3_js_1.PublicKey(this.universalSigner.account.address);
|
|
276
509
|
const priceUpdatePk = new web3_js_1.PublicKey('7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE');
|
|
@@ -279,11 +512,6 @@ class Orchestrator {
|
|
|
279
512
|
throw new Error('Pay-with token is not supported on Solana');
|
|
280
513
|
}
|
|
281
514
|
let txSignature;
|
|
282
|
-
// SVM-specific RevertSettings: bytes must be a Buffer
|
|
283
|
-
const revertSvm = {
|
|
284
|
-
fundRecipient: userPk,
|
|
285
|
-
revertMsg: Buffer.from([]),
|
|
286
|
-
};
|
|
287
515
|
// New gateway expects EVM recipient as [u8; 20]
|
|
288
516
|
const recipientEvm20 = Array.from(Buffer.from(execute.to.slice(2).padStart(40, '0'), 'hex').subarray(0, 20));
|
|
289
517
|
this.executeProgressHook(progress_hook_types_1.PROGRESS_HOOK.SEND_TX_03_01);
|
|
@@ -298,18 +526,19 @@ class Orchestrator {
|
|
|
298
526
|
token: web3_js_1.PublicKey.default,
|
|
299
527
|
amount: bridgeAmount,
|
|
300
528
|
payload: '0x',
|
|
301
|
-
|
|
529
|
+
revertRecipient: userPk,
|
|
302
530
|
signatureData: '0x',
|
|
303
531
|
});
|
|
304
532
|
txSignature = yield svmClient.writeContract({
|
|
305
533
|
abi: abi_1.SVM_GATEWAY_IDL,
|
|
306
534
|
address: programId.toBase58(),
|
|
307
535
|
functionName: 'sendUniversalTx',
|
|
308
|
-
args: [reqNative, bridgeAmount],
|
|
536
|
+
args: [reqNative, bridgeAmount + protocolFeeLamports],
|
|
309
537
|
signer: this.universalSigner,
|
|
310
538
|
accounts: {
|
|
311
539
|
config: configPda,
|
|
312
540
|
vault: vaultPda,
|
|
541
|
+
feeVault: feeVaultPda,
|
|
313
542
|
userTokenAccount: vaultPda,
|
|
314
543
|
gatewayTokenAccount: vaultPda,
|
|
315
544
|
user: userPk,
|
|
@@ -344,18 +573,19 @@ class Orchestrator {
|
|
|
344
573
|
token: mintPk,
|
|
345
574
|
amount: bridgeAmount,
|
|
346
575
|
payload: '0x',
|
|
347
|
-
|
|
576
|
+
revertRecipient: userPk,
|
|
348
577
|
signatureData: '0x',
|
|
349
578
|
});
|
|
350
579
|
txSignature = yield svmClient.writeContract({
|
|
351
580
|
abi: abi_1.SVM_GATEWAY_IDL,
|
|
352
581
|
address: programId.toBase58(),
|
|
353
582
|
functionName: 'sendUniversalTx',
|
|
354
|
-
args: [reqSpl,
|
|
583
|
+
args: [reqSpl, protocolFeeLamports],
|
|
355
584
|
signer: this.universalSigner,
|
|
356
585
|
accounts: {
|
|
357
586
|
config: configPda,
|
|
358
587
|
vault: vaultPda,
|
|
588
|
+
feeVault: feeVaultPda,
|
|
359
589
|
userTokenAccount: userAta,
|
|
360
590
|
gatewayTokenAccount: vaultAta,
|
|
361
591
|
user: userPk,
|
|
@@ -375,18 +605,19 @@ class Orchestrator {
|
|
|
375
605
|
token: mintPk,
|
|
376
606
|
amount: bridgeAmount,
|
|
377
607
|
payload: '0x',
|
|
378
|
-
|
|
608
|
+
revertRecipient: userPk,
|
|
379
609
|
signatureData: '0x',
|
|
380
610
|
});
|
|
381
611
|
txSignature = yield svmClient.writeContract({
|
|
382
612
|
abi: abi_1.SVM_GATEWAY_IDL,
|
|
383
613
|
address: programId.toBase58(),
|
|
384
614
|
functionName: 'sendUniversalTx',
|
|
385
|
-
args: [reqSpl,
|
|
615
|
+
args: [reqSpl, protocolFeeLamports],
|
|
386
616
|
signer: this.universalSigner,
|
|
387
617
|
accounts: {
|
|
388
618
|
config: configPda,
|
|
389
619
|
vault: vaultPda,
|
|
620
|
+
feeVault: feeVaultPda,
|
|
390
621
|
userTokenAccount: userAta,
|
|
391
622
|
gatewayTokenAccount: vaultAta,
|
|
392
623
|
user: userPk,
|
|
@@ -411,7 +642,7 @@ class Orchestrator {
|
|
|
411
642
|
this.executeProgressHook(progress_hook_types_1.PROGRESS_HOOK.SEND_TX_06_05);
|
|
412
643
|
// After origin confirmations, query Push Chain for UniversalTx status (SVM)
|
|
413
644
|
const pushChainUniversalTx = yield this.queryUniversalTxStatusFromGatewayTx(undefined, undefined, txSignature, 'sendFunds');
|
|
414
|
-
if (!((
|
|
645
|
+
if (!((_f = pushChainUniversalTx === null || pushChainUniversalTx === void 0 ? void 0 : pushChainUniversalTx.pcTx) === null || _f === void 0 ? void 0 : _f.length)) {
|
|
415
646
|
throw new Error(`Failed to retrieve Push Chain transaction status for gateway tx: ${txSignature}. ` +
|
|
416
647
|
`The transaction may have failed on Push Chain or not been indexed yet.`);
|
|
417
648
|
}
|
|
@@ -459,7 +690,7 @@ class Orchestrator {
|
|
|
459
690
|
const gasEstimate = execute.gasLimit || BigInt(1e7);
|
|
460
691
|
const gasPrice = yield this.pushClient.getGasPrice();
|
|
461
692
|
const requiredGasFee = gasEstimate * gasPrice;
|
|
462
|
-
const payloadValue = (
|
|
693
|
+
const payloadValue = (_g = execute.value) !== null && _g !== void 0 ? _g : BigInt(0);
|
|
463
694
|
const requiredFunds = requiredGasFee + payloadValue;
|
|
464
695
|
const ueaAddress = this.computeUEAOffchain();
|
|
465
696
|
const [ueaBalance] = yield Promise.all([
|
|
@@ -492,8 +723,8 @@ class Orchestrator {
|
|
|
492
723
|
functionName: 'config',
|
|
493
724
|
args: [configPda.toBase58()],
|
|
494
725
|
});
|
|
495
|
-
const minField = (
|
|
496
|
-
const maxField = (
|
|
726
|
+
const minField = (_h = cfg.minCapUniversalTxUsd) !== null && _h !== void 0 ? _h : cfg.min_cap_universal_tx_usd;
|
|
727
|
+
const maxField = (_j = cfg.maxCapUniversalTxUsd) !== null && _j !== void 0 ? _j : cfg.max_cap_universal_tx_usd;
|
|
497
728
|
const minCapUsd = BigInt(minField.toString());
|
|
498
729
|
const maxCapUsd = BigInt(maxField.toString());
|
|
499
730
|
if (depositUsd < minCapUsd)
|
|
@@ -505,7 +736,7 @@ class Orchestrator {
|
|
|
505
736
|
if (depositUsd > maxCapUsd)
|
|
506
737
|
depositUsd = maxCapUsd;
|
|
507
738
|
}
|
|
508
|
-
catch (
|
|
739
|
+
catch (_t) {
|
|
509
740
|
// best-effort; fallback to previous bounds if read fails
|
|
510
741
|
}
|
|
511
742
|
}
|
|
@@ -556,7 +787,7 @@ class Orchestrator {
|
|
|
556
787
|
// New behavior: if user provided a gasTokenAddress, pay gas in that token via Uniswap quote
|
|
557
788
|
// Determine pay-with token address, min-out and slippage
|
|
558
789
|
const payWith = execute.payGasWith;
|
|
559
|
-
const gasTokenAddress = (
|
|
790
|
+
const gasTokenAddress = (_k = payWith === null || payWith === void 0 ? void 0 : payWith.token) === null || _k === void 0 ? void 0 : _k.address;
|
|
560
791
|
if (gasTokenAddress) {
|
|
561
792
|
if (chain !== enums_1.CHAIN.ETHEREUM_SEPOLIA) {
|
|
562
793
|
throw new Error(`Only ${push_chain_1.PushChain.utils.chains.getChainName(enums_1.CHAIN.ETHEREUM_SEPOLIA)} is supported for paying gas fees with ERC-20 tokens`);
|
|
@@ -564,7 +795,7 @@ class Orchestrator {
|
|
|
564
795
|
let amountOutMinETH = (payWith === null || payWith === void 0 ? void 0 : payWith.minAmountOut) !== undefined
|
|
565
796
|
? BigInt(payWith.minAmountOut)
|
|
566
797
|
: nativeAmount;
|
|
567
|
-
const slippageBps = (
|
|
798
|
+
const slippageBps = (_l = payWith === null || payWith === void 0 ? void 0 : payWith.slippageBps) !== null && _l !== void 0 ? _l : 100;
|
|
568
799
|
amountOutMinETH = BigInt(push_chain_1.PushChain.utils.conversion.slippageToMinAmount(amountOutMinETH.toString(), { slippageBps }));
|
|
569
800
|
const { gasAmount } = yield this.calculateGasAmountFromAmountOutMinETH(gasTokenAddress, amountOutMinETH);
|
|
570
801
|
const deadline = BigInt(0);
|
|
@@ -576,8 +807,8 @@ class Orchestrator {
|
|
|
576
807
|
ownerAddress,
|
|
577
808
|
});
|
|
578
809
|
if (gasTokenBalance < gasAmount) {
|
|
579
|
-
const sym = (
|
|
580
|
-
const decimals = (
|
|
810
|
+
const sym = (_o = (_m = payWith === null || payWith === void 0 ? void 0 : payWith.token) === null || _m === void 0 ? void 0 : _m.symbol) !== null && _o !== void 0 ? _o : 'gas token';
|
|
811
|
+
const decimals = (_q = (_p = payWith === null || payWith === void 0 ? void 0 : payWith.token) === null || _p === void 0 ? void 0 : _p.decimals) !== null && _q !== void 0 ? _q : 18;
|
|
581
812
|
const needFmt = utils_1.Utils.helpers.formatUnits(gasAmount, decimals);
|
|
582
813
|
const haveFmt = utils_1.Utils.helpers.formatUnits(gasTokenBalance, decimals);
|
|
583
814
|
throw new Error(`Insufficient ${sym} balance to cover gas fees: need ${needFmt}, have ${haveFmt}`);
|
|
@@ -600,13 +831,7 @@ class Orchestrator {
|
|
|
600
831
|
const reqToken = Object.assign(Object.assign({}, req), { gasToken: gasTokenAddress, gasAmount,
|
|
601
832
|
amountOutMinETH,
|
|
602
833
|
deadline });
|
|
603
|
-
txHash = yield evmClientEvm.
|
|
604
|
-
abi: abi_1.UNIVERSAL_GATEWAY_V0,
|
|
605
|
-
address: gatewayAddressEvm,
|
|
606
|
-
functionName: 'sendUniversalTx',
|
|
607
|
-
args: [reqToken],
|
|
608
|
-
signer: this.universalSigner,
|
|
609
|
-
});
|
|
834
|
+
txHash = yield this.sendGatewayTokenTxWithFallback(evmClientEvm, gatewayAddressEvm, reqToken, this.universalSigner);
|
|
610
835
|
}
|
|
611
836
|
else {
|
|
612
837
|
// Existing native-ETH value path
|
|
@@ -631,14 +856,7 @@ class Orchestrator {
|
|
|
631
856
|
const totalValue = isNativeToken
|
|
632
857
|
? nativeAmount + bridgeAmount
|
|
633
858
|
: nativeAmount;
|
|
634
|
-
txHash = yield evmClientEvm.
|
|
635
|
-
abi: abi_1.UNIVERSAL_GATEWAY_V0,
|
|
636
|
-
address: gatewayAddressEvm,
|
|
637
|
-
functionName: 'sendUniversalTx',
|
|
638
|
-
args: [req],
|
|
639
|
-
signer: this.universalSigner,
|
|
640
|
-
value: totalValue,
|
|
641
|
-
});
|
|
859
|
+
txHash = yield this.sendGatewayTxWithFallback(evmClientEvm, gatewayAddressEvm, req, this.universalSigner, totalValue);
|
|
642
860
|
}
|
|
643
861
|
}
|
|
644
862
|
else {
|
|
@@ -705,7 +923,7 @@ class Orchestrator {
|
|
|
705
923
|
pushChainUniversalTx =
|
|
706
924
|
yield this.queryUniversalTxStatusFromGatewayTx(undefined, undefined, txHash, 'sendTxWithFunds');
|
|
707
925
|
}
|
|
708
|
-
if (!((
|
|
926
|
+
if (!((_r = pushChainUniversalTx === null || pushChainUniversalTx === void 0 ? void 0 : pushChainUniversalTx.pcTx) === null || _r === void 0 ? void 0 : _r.length)) {
|
|
709
927
|
throw new Error(`Failed to retrieve Push Chain transaction status for gateway tx: ${txHash}. ` +
|
|
710
928
|
`The transaction may have failed on Push Chain or not been indexed yet.`);
|
|
711
929
|
}
|
|
@@ -752,17 +970,32 @@ class Orchestrator {
|
|
|
752
970
|
const requiredFunds = requiredGasFee + execute.value;
|
|
753
971
|
this.executeProgressHook(progress_hook_types_1.PROGRESS_HOOK.SEND_TX_02_02, requiredFunds);
|
|
754
972
|
/**
|
|
755
|
-
* Fetch UEA Details
|
|
973
|
+
* Fetch UEA Details (or use pre-fetched status if available)
|
|
756
974
|
*/
|
|
757
|
-
this.executeProgressHook(progress_hook_types_1.PROGRESS_HOOK.SEND_TX_03_01);
|
|
758
975
|
const UEA = this.computeUEAOffchain();
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
976
|
+
let isUEADeployed;
|
|
977
|
+
let nonce;
|
|
978
|
+
let funds;
|
|
979
|
+
if (execute._ueaStatus) {
|
|
980
|
+
// Use pre-fetched UEA status (from executeUoaToCea)
|
|
981
|
+
isUEADeployed = execute._ueaStatus.isDeployed;
|
|
982
|
+
nonce = execute._ueaStatus.nonce;
|
|
983
|
+
funds = execute._ueaStatus.balance;
|
|
984
|
+
this.executeProgressHook(progress_hook_types_1.PROGRESS_HOOK.SEND_TX_03_01);
|
|
985
|
+
this.executeProgressHook(progress_hook_types_1.PROGRESS_HOOK.SEND_TX_03_02, UEA, isUEADeployed);
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
// Fetch UEA status from Push Chain RPC
|
|
989
|
+
this.executeProgressHook(progress_hook_types_1.PROGRESS_HOOK.SEND_TX_03_01);
|
|
990
|
+
const [code, balance] = yield Promise.all([
|
|
991
|
+
this.pushClient.publicClient.getCode({ address: UEA }),
|
|
992
|
+
this.pushClient.getBalance(UEA),
|
|
993
|
+
]);
|
|
994
|
+
isUEADeployed = code !== undefined;
|
|
995
|
+
nonce = isUEADeployed ? yield this.getUEANonce(UEA) : BigInt(0);
|
|
996
|
+
funds = balance;
|
|
997
|
+
this.executeProgressHook(progress_hook_types_1.PROGRESS_HOOK.SEND_TX_03_02, UEA, isUEADeployed);
|
|
998
|
+
}
|
|
766
999
|
/**
|
|
767
1000
|
* Compute Universal Payload Hash
|
|
768
1001
|
*/
|
|
@@ -772,9 +1005,11 @@ class Orchestrator {
|
|
|
772
1005
|
const decoded = anchor_1.utils.bytes.bs58.decode(feeLockTxHash);
|
|
773
1006
|
feeLockTxHash = (0, viem_1.bytesToHex)(new Uint8Array(decoded));
|
|
774
1007
|
}
|
|
775
|
-
// Fee locking is required if UEA is not deployed OR insufficient funds
|
|
776
|
-
|
|
777
|
-
//
|
|
1008
|
+
// Fee locking is required if UEA is not deployed OR insufficient funds.
|
|
1009
|
+
// Skip for outbound flows (UEA→CEA) — those execute on Push Chain and
|
|
1010
|
+
// don't need external-chain fee locking.
|
|
1011
|
+
const feeLockingRequired = !execute._skipFeeLocking &&
|
|
1012
|
+
(!isUEADeployed || funds < requiredFunds) && !feeLockTxHash;
|
|
778
1013
|
// Support multicall payload encoding when execute.data is an array
|
|
779
1014
|
let payloadData;
|
|
780
1015
|
let payloadTo;
|
|
@@ -931,7 +1166,7 @@ class Orchestrator {
|
|
|
931
1166
|
* Note: queryTx may be undefined since validators don't recognize new UniversalTx event yet
|
|
932
1167
|
*/
|
|
933
1168
|
// Transform to UniversalTxResponse (follow sendFunds pattern)
|
|
934
|
-
if (!((
|
|
1169
|
+
if (!((_s = pushChainUniversalTx === null || pushChainUniversalTx === void 0 ? void 0 : pushChainUniversalTx.pcTx) === null || _s === void 0 ? void 0 : _s.length)) {
|
|
935
1170
|
throw new Error(`Failed to retrieve Push Chain transaction status for gateway tx: ${feeLockTxHash}. ` +
|
|
936
1171
|
`The transaction may have failed on Push Chain or not been indexed yet.`);
|
|
937
1172
|
}
|
|
@@ -967,19 +1202,2179 @@ class Orchestrator {
|
|
|
967
1202
|
try {
|
|
968
1203
|
return JSON.stringify(err);
|
|
969
1204
|
}
|
|
970
|
-
catch (_a) {
|
|
971
|
-
return 'Unknown error';
|
|
1205
|
+
catch (_a) {
|
|
1206
|
+
return 'Unknown error';
|
|
1207
|
+
}
|
|
1208
|
+
})();
|
|
1209
|
+
this.executeProgressHook(progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_02, errMessage);
|
|
1210
|
+
throw err;
|
|
1211
|
+
}
|
|
1212
|
+
finally {
|
|
1213
|
+
// Restore original progressHook
|
|
1214
|
+
this.progressHook = originalHook;
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
// ============================================================================
|
|
1219
|
+
// Multi-Chain Universal Transaction Methods
|
|
1220
|
+
// ============================================================================
|
|
1221
|
+
/**
|
|
1222
|
+
* Executes a universal transaction with multi-chain routing support.
|
|
1223
|
+
* Supports 4 routes:
|
|
1224
|
+
* - Route 1: UOA → Push Chain (existing behavior)
|
|
1225
|
+
* - Route 2: UOA → CEA (outbound to external chain)
|
|
1226
|
+
* - Route 3: CEA → Push Chain (inbound from external chain)
|
|
1227
|
+
* - Route 4: CEA → CEA (external chain to external chain via Push)
|
|
1228
|
+
*
|
|
1229
|
+
* @param params - Universal execution parameters with optional chain targets
|
|
1230
|
+
* @returns UniversalTxResponse with chain information
|
|
1231
|
+
*/
|
|
1232
|
+
/**
|
|
1233
|
+
* Internal method for multi-chain transaction routing.
|
|
1234
|
+
* Called by execute() when ChainTarget or from.chain is detected.
|
|
1235
|
+
*/
|
|
1236
|
+
executeMultiChain(params) {
|
|
1237
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
1238
|
+
// Validate route parameters
|
|
1239
|
+
(0, route_detector_1.validateRouteParams)(params, {
|
|
1240
|
+
clientChain: this.universalSigner.account.chain,
|
|
1241
|
+
});
|
|
1242
|
+
// Detect the transaction route
|
|
1243
|
+
const route = (0, route_detector_1.detectRoute)(params);
|
|
1244
|
+
this.printLog(`executeMultiChain — detected route: ${route}, params: ${JSON.stringify({
|
|
1245
|
+
to: typeof params.to === 'string'
|
|
1246
|
+
? params.to
|
|
1247
|
+
: { address: params.to.address, chain: params.to.chain },
|
|
1248
|
+
from: params.from,
|
|
1249
|
+
hasValue: params.value !== undefined,
|
|
1250
|
+
hasData: params.data !== undefined,
|
|
1251
|
+
hasFunds: params.funds !== undefined,
|
|
1252
|
+
}, null, 2)}`);
|
|
1253
|
+
let response;
|
|
1254
|
+
switch (route) {
|
|
1255
|
+
case route_detector_1.TransactionRoute.UOA_TO_PUSH:
|
|
1256
|
+
// Route 1: Standard Push Chain execution
|
|
1257
|
+
response = yield this.execute(this.toExecuteParams(params));
|
|
1258
|
+
break;
|
|
1259
|
+
case route_detector_1.TransactionRoute.UOA_TO_CEA:
|
|
1260
|
+
// Route 2: Outbound to external chain via CEA
|
|
1261
|
+
response = yield this.executeUoaToCea(params);
|
|
1262
|
+
break;
|
|
1263
|
+
case route_detector_1.TransactionRoute.CEA_TO_PUSH:
|
|
1264
|
+
// Route 3: Inbound from CEA to Push Chain
|
|
1265
|
+
response = yield this.executeCeaToPush(params);
|
|
1266
|
+
break;
|
|
1267
|
+
case route_detector_1.TransactionRoute.CEA_TO_CEA:
|
|
1268
|
+
// Route 4: CEA to CEA via Push Chain
|
|
1269
|
+
response = yield this.executeCeaToCea(params);
|
|
1270
|
+
break;
|
|
1271
|
+
default:
|
|
1272
|
+
throw new Error(`Unknown transaction route: ${route}`);
|
|
1273
|
+
}
|
|
1274
|
+
// Set the route on the response for .wait() to use
|
|
1275
|
+
response.route = route;
|
|
1276
|
+
return response;
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Prepare a universal transaction without executing it.
|
|
1281
|
+
* Returns a PreparedUniversalTx that can be chained with thenOn() or sent with send().
|
|
1282
|
+
*
|
|
1283
|
+
* @param params - Universal execution parameters
|
|
1284
|
+
* @returns PreparedUniversalTx with chaining capabilities
|
|
1285
|
+
*/
|
|
1286
|
+
prepareTransaction(params) {
|
|
1287
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
1288
|
+
(0, route_detector_1.validateRouteParams)(params, {
|
|
1289
|
+
clientChain: this.universalSigner.account.chain,
|
|
1290
|
+
});
|
|
1291
|
+
const route = (0, route_detector_1.detectRoute)(params);
|
|
1292
|
+
const { nonce, deployed } = yield this.getUeaStatusAndNonce();
|
|
1293
|
+
const ueaAddress = this.computeUEAOffchain();
|
|
1294
|
+
// Build the payload based on route
|
|
1295
|
+
const { payload, gatewayRequest } = yield this.buildPayloadForRoute(params, route, nonce);
|
|
1296
|
+
const gasEstimate = params.gasLimit || selectors_1.DEFAULT_OUTBOUND_GAS_LIMIT;
|
|
1297
|
+
const deadline = params.deadline || BigInt(Math.floor(Date.now() / 1000) + 3600);
|
|
1298
|
+
// Build the HopDescriptor with all metadata needed for cascade nesting
|
|
1299
|
+
const hop = yield this.buildHopDescriptor(params, route, ueaAddress);
|
|
1300
|
+
const prepared = {
|
|
1301
|
+
route,
|
|
1302
|
+
payload,
|
|
1303
|
+
gatewayRequest,
|
|
1304
|
+
estimatedGas: gasEstimate,
|
|
1305
|
+
nonce,
|
|
1306
|
+
deadline,
|
|
1307
|
+
_hop: hop,
|
|
1308
|
+
thenOn: (nextTx) => this.createCascadedBuilder([prepared, nextTx]),
|
|
1309
|
+
send: () => this.executeMultiChain(params),
|
|
1310
|
+
};
|
|
1311
|
+
return prepared;
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Build a HopDescriptor for a prepared transaction.
|
|
1316
|
+
* Resolves CEA addresses, queries gas fees, and builds multicall arrays.
|
|
1317
|
+
*
|
|
1318
|
+
* @param params - Original user params
|
|
1319
|
+
* @param route - Detected route
|
|
1320
|
+
* @param ueaAddress - UEA address
|
|
1321
|
+
* @returns HopDescriptor with all metadata
|
|
1322
|
+
*/
|
|
1323
|
+
buildHopDescriptor(params, route, ueaAddress) {
|
|
1324
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
1325
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
1326
|
+
// Pass 0 when user omits gasLimit → contract uses per-chain baseGasLimitByChainNamespace
|
|
1327
|
+
const gasLimit = (_a = params.gasLimit) !== null && _a !== void 0 ? _a : BigInt(0);
|
|
1328
|
+
const routeStr = route;
|
|
1329
|
+
const baseDescriptor = {
|
|
1330
|
+
params,
|
|
1331
|
+
route: routeStr,
|
|
1332
|
+
gasLimit,
|
|
1333
|
+
ueaAddress,
|
|
1334
|
+
revertRecipient: ueaAddress,
|
|
1335
|
+
};
|
|
1336
|
+
switch (route) {
|
|
1337
|
+
case route_detector_1.TransactionRoute.UOA_TO_PUSH: {
|
|
1338
|
+
// Route 1: Build Push Chain multicalls
|
|
1339
|
+
const executeParams = this.toExecuteParams(params);
|
|
1340
|
+
const pushMulticalls = (0, payload_builders_1.buildExecuteMulticall)({
|
|
1341
|
+
execute: executeParams,
|
|
1342
|
+
ueaAddress,
|
|
1343
|
+
});
|
|
1344
|
+
return Object.assign(Object.assign({}, baseDescriptor), { pushMulticalls });
|
|
1345
|
+
}
|
|
1346
|
+
case route_detector_1.TransactionRoute.UOA_TO_CEA: {
|
|
1347
|
+
// Route 2: Build outbound metadata
|
|
1348
|
+
const target = params.to;
|
|
1349
|
+
const targetChain = target.chain;
|
|
1350
|
+
// Branch: SVM vs EVM
|
|
1351
|
+
if ((0, payload_builders_1.isSvmChain)(targetChain)) {
|
|
1352
|
+
// SVM path: no CEA lookup, build SVM payload
|
|
1353
|
+
const hasSvmExecute = !!params.svmExecute;
|
|
1354
|
+
let svmPayload = '0x';
|
|
1355
|
+
if (hasSvmExecute) {
|
|
1356
|
+
const exec = params.svmExecute;
|
|
1357
|
+
svmPayload = (0, payload_builders_1.encodeSvmExecutePayload)({
|
|
1358
|
+
targetProgram: exec.targetProgram,
|
|
1359
|
+
accounts: exec.accounts,
|
|
1360
|
+
ixData: exec.ixData,
|
|
1361
|
+
instructionId: 2,
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
let prc20Token = selectors_1.ZERO_ADDRESS;
|
|
1365
|
+
let burnAmount = BigInt(0);
|
|
1366
|
+
if ((_b = params.funds) === null || _b === void 0 ? void 0 : _b.amount) {
|
|
1367
|
+
const token = params.funds.token;
|
|
1368
|
+
if (token) {
|
|
1369
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
1370
|
+
burnAmount = params.funds.amount;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
1374
|
+
prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
1375
|
+
burnAmount = params.value;
|
|
1376
|
+
}
|
|
1377
|
+
else if (hasSvmExecute) {
|
|
1378
|
+
prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
1379
|
+
burnAmount = BigInt(1);
|
|
1380
|
+
}
|
|
1381
|
+
let gasToken = selectors_1.ZERO_ADDRESS;
|
|
1382
|
+
let gasFee = BigInt(0);
|
|
1383
|
+
if (prc20Token !== selectors_1.ZERO_ADDRESS) {
|
|
1384
|
+
const result = yield this.queryOutboundGasFee(prc20Token, gasLimit);
|
|
1385
|
+
gasToken = result.gasToken;
|
|
1386
|
+
gasFee = result.gasFee;
|
|
1387
|
+
}
|
|
1388
|
+
return Object.assign(Object.assign({}, baseDescriptor), { targetChain, isSvmTarget: true, svmPayload,
|
|
1389
|
+
prc20Token,
|
|
1390
|
+
burnAmount,
|
|
1391
|
+
gasToken,
|
|
1392
|
+
gasFee });
|
|
1393
|
+
}
|
|
1394
|
+
// EVM path: Resolve CEA address + build CEA multicalls
|
|
1395
|
+
const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_c = this.rpcUrls[targetChain]) === null || _c === void 0 ? void 0 : _c[0]);
|
|
1396
|
+
// Migration path: raw MIGRATION_SELECTOR payload, no multicall wrapping
|
|
1397
|
+
if (params.migration) {
|
|
1398
|
+
const prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
1399
|
+
const burnAmount = BigInt(0); // Migration is logic-only — no funds. CEA rejects msg.value != 0.
|
|
1400
|
+
let gasToken = selectors_1.ZERO_ADDRESS;
|
|
1401
|
+
let gasFee = BigInt(0);
|
|
1402
|
+
if (prc20Token !== selectors_1.ZERO_ADDRESS) {
|
|
1403
|
+
const result = yield this.queryOutboundGasFee(prc20Token, gasLimit);
|
|
1404
|
+
gasToken = result.gasToken;
|
|
1405
|
+
gasFee = result.gasFee;
|
|
1406
|
+
}
|
|
1407
|
+
return Object.assign(Object.assign({}, baseDescriptor), { targetChain,
|
|
1408
|
+
ceaAddress, ceaMulticalls: [], prc20Token,
|
|
1409
|
+
burnAmount,
|
|
1410
|
+
gasToken,
|
|
1411
|
+
gasFee, isMigration: true });
|
|
1412
|
+
}
|
|
1413
|
+
// Build CEA multicalls
|
|
1414
|
+
const ceaMulticalls = [];
|
|
1415
|
+
if (params.data) {
|
|
1416
|
+
if (Array.isArray(params.data)) {
|
|
1417
|
+
ceaMulticalls.push(...params.data);
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
// When ERC-20 funds are provided with a single payload, auto-prepend a
|
|
1421
|
+
// transfer() call so the tokens minted to the CEA are forwarded to the
|
|
1422
|
+
// target address. This mirrors the Route 1 behavior in buildExecuteMulticall.
|
|
1423
|
+
if ((_d = params.funds) === null || _d === void 0 ? void 0 : _d.amount) {
|
|
1424
|
+
const token = params.funds.token;
|
|
1425
|
+
if (token && token.mechanism !== 'native') {
|
|
1426
|
+
const erc20Transfer = (0, viem_1.encodeFunctionData)({
|
|
1427
|
+
abi: abi_1.ERC20_EVM,
|
|
1428
|
+
functionName: 'transfer',
|
|
1429
|
+
args: [target.address, params.funds.amount],
|
|
1430
|
+
});
|
|
1431
|
+
ceaMulticalls.push({
|
|
1432
|
+
to: token.address,
|
|
1433
|
+
value: BigInt(0),
|
|
1434
|
+
data: erc20Transfer,
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
// Single call with data. Forward native value (if any) so the target
|
|
1439
|
+
// contract receives it alongside the payload call. The vault deposits
|
|
1440
|
+
// native value to the CEA, and the multicall forwards it to the target.
|
|
1441
|
+
ceaMulticalls.push({
|
|
1442
|
+
to: target.address,
|
|
1443
|
+
value: (_e = params.value) !== null && _e !== void 0 ? _e : BigInt(0),
|
|
1444
|
+
data: params.data,
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
else if (params.value) {
|
|
1449
|
+
// Skip multicall when sending native value to own CEA — gateway deposits directly.
|
|
1450
|
+
// Self-call with value would revert (CEA._handleMulticall rejects it).
|
|
1451
|
+
if (target.address.toLowerCase() !== ceaAddress.toLowerCase()) {
|
|
1452
|
+
ceaMulticalls.push({
|
|
1453
|
+
to: target.address,
|
|
1454
|
+
value: params.value,
|
|
1455
|
+
data: '0x',
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
// Determine PRC-20 token and burn amount
|
|
1460
|
+
let prc20Token = selectors_1.ZERO_ADDRESS;
|
|
1461
|
+
let burnAmount = BigInt(0);
|
|
1462
|
+
if ((_f = params.funds) === null || _f === void 0 ? void 0 : _f.amount) {
|
|
1463
|
+
const token = params.funds.token;
|
|
1464
|
+
if (token) {
|
|
1465
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
1466
|
+
burnAmount = params.funds.amount;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
1470
|
+
prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
1471
|
+
burnAmount = params.value;
|
|
1472
|
+
}
|
|
1473
|
+
else if (params.data) {
|
|
1474
|
+
prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
1475
|
+
burnAmount = BigInt(1); // Minimum for precompile
|
|
1476
|
+
}
|
|
1477
|
+
// Query gas fee
|
|
1478
|
+
let gasToken = selectors_1.ZERO_ADDRESS;
|
|
1479
|
+
let gasFee = BigInt(0);
|
|
1480
|
+
if (prc20Token !== selectors_1.ZERO_ADDRESS) {
|
|
1481
|
+
const result = yield this.queryOutboundGasFee(prc20Token, gasLimit);
|
|
1482
|
+
gasToken = result.gasToken;
|
|
1483
|
+
gasFee = result.gasFee;
|
|
1484
|
+
}
|
|
1485
|
+
return Object.assign(Object.assign({}, baseDescriptor), { targetChain,
|
|
1486
|
+
ceaAddress,
|
|
1487
|
+
ceaMulticalls,
|
|
1488
|
+
prc20Token,
|
|
1489
|
+
burnAmount,
|
|
1490
|
+
gasToken,
|
|
1491
|
+
gasFee });
|
|
1492
|
+
}
|
|
1493
|
+
case route_detector_1.TransactionRoute.CEA_TO_PUSH: {
|
|
1494
|
+
// Route 3: Build CEA multicalls for sendUniversalTxFromCEA
|
|
1495
|
+
const sourceChain = params.from.chain;
|
|
1496
|
+
// SVM chains use PDA-based CEA, not factory-deployed CEA
|
|
1497
|
+
if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
|
|
1498
|
+
const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
|
|
1499
|
+
if (!lockerContract) {
|
|
1500
|
+
throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
|
|
1501
|
+
}
|
|
1502
|
+
const programPk = new web3_js_1.PublicKey(lockerContract);
|
|
1503
|
+
const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
|
|
1504
|
+
// Derive CEA PDA
|
|
1505
|
+
const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex');
|
|
1506
|
+
const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
|
|
1507
|
+
const ceaPdaHex = ('0x' + Buffer.from(ceaPda.toBytes()).toString('hex'));
|
|
1508
|
+
let amount = BigInt(0);
|
|
1509
|
+
let prc20Token;
|
|
1510
|
+
if (((_g = params.funds) === null || _g === void 0 ? void 0 : _g.amount) && params.funds.amount > BigInt(0)) {
|
|
1511
|
+
amount = params.funds.amount;
|
|
1512
|
+
const token = params.funds.token;
|
|
1513
|
+
if (token && token.address) {
|
|
1514
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
1515
|
+
}
|
|
1516
|
+
else {
|
|
1517
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
1521
|
+
amount = params.value;
|
|
1522
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
1523
|
+
}
|
|
1524
|
+
else {
|
|
1525
|
+
// Payload-only Route 3 SVM
|
|
1526
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
1527
|
+
}
|
|
1528
|
+
let gasToken = selectors_1.ZERO_ADDRESS;
|
|
1529
|
+
let gasFee = BigInt(0);
|
|
1530
|
+
if (prc20Token !== selectors_1.ZERO_ADDRESS) {
|
|
1531
|
+
const result = yield this.queryOutboundGasFee(prc20Token, gasLimit);
|
|
1532
|
+
gasToken = result.gasToken;
|
|
1533
|
+
gasFee = result.gasFee;
|
|
1534
|
+
}
|
|
1535
|
+
return Object.assign(Object.assign({}, baseDescriptor), { sourceChain, ceaAddress: ceaPdaHex, isSvmTarget: true, prc20Token, burnAmount: amount > BigInt(0) ? amount : BigInt(1), gasToken,
|
|
1536
|
+
gasFee });
|
|
1537
|
+
}
|
|
1538
|
+
const { cea: ceaAddress, isDeployed } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_h = this.rpcUrls[sourceChain]) === null || _h === void 0 ? void 0 : _h[0]);
|
|
1539
|
+
// Determine token/amount for the inbound
|
|
1540
|
+
let tokenAddress = selectors_1.ZERO_ADDRESS;
|
|
1541
|
+
let amount = BigInt(0);
|
|
1542
|
+
let nativeValue = BigInt(0);
|
|
1543
|
+
if ((_j = params.funds) === null || _j === void 0 ? void 0 : _j.amount) {
|
|
1544
|
+
const token = params.funds.token;
|
|
1545
|
+
if (token) {
|
|
1546
|
+
if (token.mechanism === 'native') {
|
|
1547
|
+
amount = params.funds.amount;
|
|
1548
|
+
nativeValue = params.funds.amount;
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
tokenAddress = token.address;
|
|
1552
|
+
amount = params.funds.amount;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
1557
|
+
amount = params.value;
|
|
1558
|
+
nativeValue = params.value;
|
|
1559
|
+
}
|
|
1560
|
+
// The PRC-20 for the outbound wrapper (Route 2 to source chain)
|
|
1561
|
+
const prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
1562
|
+
let gasToken = selectors_1.ZERO_ADDRESS;
|
|
1563
|
+
let gasFee = BigInt(0);
|
|
1564
|
+
if (prc20Token !== selectors_1.ZERO_ADDRESS) {
|
|
1565
|
+
const result = yield this.queryOutboundGasFee(prc20Token, gasLimit);
|
|
1566
|
+
gasToken = result.gasToken;
|
|
1567
|
+
gasFee = result.gasFee;
|
|
1568
|
+
}
|
|
1569
|
+
return Object.assign(Object.assign({}, baseDescriptor), { sourceChain,
|
|
1570
|
+
ceaAddress,
|
|
1571
|
+
prc20Token, burnAmount: amount > BigInt(0) ? amount : BigInt(1), gasToken,
|
|
1572
|
+
gasFee });
|
|
1573
|
+
}
|
|
1574
|
+
default:
|
|
1575
|
+
return baseDescriptor;
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Query outbound gas fee from UniversalCore contract.
|
|
1581
|
+
* Extracted from executeUoaToCea for reuse.
|
|
1582
|
+
*
|
|
1583
|
+
* @param prc20Token - PRC-20 token address
|
|
1584
|
+
* @param gasLimit - Gas limit for the outbound
|
|
1585
|
+
* @returns gasToken address and gasFee amount
|
|
1586
|
+
*/
|
|
1587
|
+
queryOutboundGasFee(prc20Token, gasLimit) {
|
|
1588
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
1589
|
+
var _a, _b;
|
|
1590
|
+
const gatewayPcAddress = this.getUniversalGatewayPCAddress();
|
|
1591
|
+
const pushChain = this.getPushChainForNetwork();
|
|
1592
|
+
const rpcUrl = ((_b = (_a = chain_1.CHAIN_INFO[pushChain]) === null || _a === void 0 ? void 0 : _a.defaultRPC) === null || _b === void 0 ? void 0 : _b[0]) || 'unknown';
|
|
1593
|
+
this.printLog(`queryOutboundGasFee — [step 1] inputs: gateway=${gatewayPcAddress}, prc20Token=${prc20Token}, gasLimit=${gasLimit}, pushNetwork=${this.pushNetwork}, rpcUrl=${rpcUrl}`);
|
|
1594
|
+
// Step 2: Get UNIVERSAL_CORE address from gateway
|
|
1595
|
+
let universalCoreAddress;
|
|
1596
|
+
try {
|
|
1597
|
+
const gatewayCallData = (0, viem_1.encodeFunctionData)({
|
|
1598
|
+
abi: abi_1.UNIVERSAL_GATEWAY_PC,
|
|
1599
|
+
functionName: 'UNIVERSAL_CORE',
|
|
1600
|
+
args: [],
|
|
1601
|
+
});
|
|
1602
|
+
this.printLog(`queryOutboundGasFee — [step 2] reading UNIVERSAL_CORE from ${gatewayPcAddress}, callData=${gatewayCallData}`);
|
|
1603
|
+
universalCoreAddress = yield this.pushClient.readContract({
|
|
1604
|
+
address: gatewayPcAddress,
|
|
1605
|
+
abi: abi_1.UNIVERSAL_GATEWAY_PC,
|
|
1606
|
+
functionName: 'UNIVERSAL_CORE',
|
|
1607
|
+
args: [],
|
|
1608
|
+
});
|
|
1609
|
+
this.printLog(`queryOutboundGasFee — [step 2] UNIVERSAL_CORE resolved to: ${universalCoreAddress}`);
|
|
1610
|
+
}
|
|
1611
|
+
catch (err) {
|
|
1612
|
+
this.printLog(`queryOutboundGasFee — [step 2] FAILED to read UNIVERSAL_CORE: ${err instanceof Error ? err.message : String(err)}`);
|
|
1613
|
+
throw err;
|
|
1614
|
+
}
|
|
1615
|
+
// Step 3: Call getOutboundTxGasAndFees on UNIVERSAL_CORE
|
|
1616
|
+
const callData = (0, viem_1.encodeFunctionData)({
|
|
1617
|
+
abi: abi_1.UNIVERSAL_CORE_EVM,
|
|
1618
|
+
functionName: 'getOutboundTxGasAndFees',
|
|
1619
|
+
args: [prc20Token, gasLimit],
|
|
1620
|
+
});
|
|
1621
|
+
this.printLog(`queryOutboundGasFee — [step 3] calling getOutboundTxGasAndFees on ${universalCoreAddress}`);
|
|
1622
|
+
this.printLog(`queryOutboundGasFee — [step 3] eth_call: {"method":"eth_call","params":[{"to":"${universalCoreAddress}","data":"${callData}"},"latest"]}`);
|
|
1623
|
+
let gasToken;
|
|
1624
|
+
let gasFee;
|
|
1625
|
+
let protocolFee;
|
|
1626
|
+
let gasPrice = BigInt(0);
|
|
1627
|
+
try {
|
|
1628
|
+
const result = yield this.pushClient.readContract({
|
|
1629
|
+
address: universalCoreAddress,
|
|
1630
|
+
abi: abi_1.UNIVERSAL_CORE_EVM,
|
|
1631
|
+
functionName: 'getOutboundTxGasAndFees',
|
|
1632
|
+
args: [prc20Token, gasLimit],
|
|
1633
|
+
});
|
|
1634
|
+
gasToken = result[0];
|
|
1635
|
+
gasFee = result[1];
|
|
1636
|
+
protocolFee = result[2];
|
|
1637
|
+
gasPrice = result[3];
|
|
1638
|
+
this.printLog(`queryOutboundGasFee — [step 4] success: gasToken=${gasToken}, gasFee=${gasFee}, protocolFee=${protocolFee}, gasPrice=${gasPrice}`);
|
|
1639
|
+
}
|
|
1640
|
+
catch (err) {
|
|
1641
|
+
this.printLog(`queryOutboundGasFee — [step 3] FAILED: ${err instanceof Error ? err.message : String(err)}`);
|
|
1642
|
+
throw err;
|
|
1643
|
+
}
|
|
1644
|
+
// gasFee is in gas token units — exchange rate to PC is unknown without quoter.
|
|
1645
|
+
// Use 1000000x buffer; excess is refunded by swapAndBurnGas to the UEA.
|
|
1646
|
+
const nativeValueForGas = protocolFee + (gasFee * BigInt(1000000));
|
|
1647
|
+
this.printLog(`queryOutboundGasFee — [step 5] using 1000000x buffer: nativeValueForGas=${nativeValueForGas}`);
|
|
1648
|
+
return { gasToken, gasFee, protocolFee, nativeValueForGas, gasPrice };
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
// ============================================================================
|
|
1652
|
+
// Cascade Composition (Advance Hopping)
|
|
1653
|
+
// ============================================================================
|
|
1654
|
+
/**
|
|
1655
|
+
* Classify hops into segments for cascade composition.
|
|
1656
|
+
* Consecutive same-type/same-chain hops are merged.
|
|
1657
|
+
*
|
|
1658
|
+
* @param hops - Array of HopDescriptors from prepared transactions
|
|
1659
|
+
* @returns Array of CascadeSegments
|
|
1660
|
+
*/
|
|
1661
|
+
classifyIntoSegments(hops) {
|
|
1662
|
+
if (hops.length === 0)
|
|
1663
|
+
return [];
|
|
1664
|
+
const segments = [];
|
|
1665
|
+
let currentSegment = null;
|
|
1666
|
+
for (const hop of hops) {
|
|
1667
|
+
const segType = this.getSegmentType(hop.route);
|
|
1668
|
+
const chain = hop.targetChain || hop.sourceChain;
|
|
1669
|
+
const canMerge = currentSegment &&
|
|
1670
|
+
currentSegment.type === segType &&
|
|
1671
|
+
// Same-chain merging for OUTBOUND_TO_CEA (EVM only — SVM hops are atomic)
|
|
1672
|
+
(segType === 'OUTBOUND_TO_CEA'
|
|
1673
|
+
? currentSegment.targetChain === hop.targetChain &&
|
|
1674
|
+
!hop.isSvmTarget
|
|
1675
|
+
: segType === 'PUSH_EXECUTION');
|
|
1676
|
+
if (canMerge && currentSegment) {
|
|
1677
|
+
// Merge into current segment
|
|
1678
|
+
currentSegment.hops.push(hop);
|
|
1679
|
+
if (segType === 'OUTBOUND_TO_CEA') {
|
|
1680
|
+
currentSegment.mergedCeaMulticalls = [
|
|
1681
|
+
...(currentSegment.mergedCeaMulticalls || []),
|
|
1682
|
+
...(hop.ceaMulticalls || []),
|
|
1683
|
+
];
|
|
1684
|
+
currentSegment.totalBurnAmount =
|
|
1685
|
+
(currentSegment.totalBurnAmount || BigInt(0)) +
|
|
1686
|
+
(hop.burnAmount || BigInt(0));
|
|
1687
|
+
// Gas fee: take the max gasLimit across merged hops
|
|
1688
|
+
if (hop.gasLimit > (currentSegment.gasLimit || BigInt(0))) {
|
|
1689
|
+
currentSegment.gasLimit = hop.gasLimit;
|
|
1690
|
+
}
|
|
1691
|
+
// Accumulate gas fees
|
|
1692
|
+
currentSegment.gasFee =
|
|
1693
|
+
(currentSegment.gasFee || BigInt(0)) +
|
|
1694
|
+
(hop.gasFee || BigInt(0));
|
|
1695
|
+
}
|
|
1696
|
+
else if (segType === 'PUSH_EXECUTION') {
|
|
1697
|
+
currentSegment.mergedPushMulticalls = [
|
|
1698
|
+
...(currentSegment.mergedPushMulticalls || []),
|
|
1699
|
+
...(hop.pushMulticalls || []),
|
|
1700
|
+
];
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
else {
|
|
1704
|
+
// Start a new segment
|
|
1705
|
+
currentSegment = {
|
|
1706
|
+
type: segType,
|
|
1707
|
+
hops: [hop],
|
|
1708
|
+
targetChain: hop.targetChain,
|
|
1709
|
+
sourceChain: hop.sourceChain,
|
|
1710
|
+
mergedCeaMulticalls: segType === 'OUTBOUND_TO_CEA' ? [...(hop.ceaMulticalls || [])] : undefined,
|
|
1711
|
+
mergedPushMulticalls: segType === 'PUSH_EXECUTION' ? [...(hop.pushMulticalls || [])] : undefined,
|
|
1712
|
+
totalBurnAmount: hop.burnAmount,
|
|
1713
|
+
prc20Token: hop.prc20Token,
|
|
1714
|
+
gasToken: hop.gasToken,
|
|
1715
|
+
gasFee: hop.gasFee,
|
|
1716
|
+
gasLimit: hop.gasLimit,
|
|
1717
|
+
};
|
|
1718
|
+
segments.push(currentSegment);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return segments;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Map route to segment type
|
|
1725
|
+
*/
|
|
1726
|
+
getSegmentType(route) {
|
|
1727
|
+
switch (route) {
|
|
1728
|
+
case 'UOA_TO_PUSH':
|
|
1729
|
+
return 'PUSH_EXECUTION';
|
|
1730
|
+
case 'UOA_TO_CEA':
|
|
1731
|
+
return 'OUTBOUND_TO_CEA';
|
|
1732
|
+
case 'CEA_TO_PUSH':
|
|
1733
|
+
return 'INBOUND_FROM_CEA';
|
|
1734
|
+
default:
|
|
1735
|
+
return 'PUSH_EXECUTION';
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Compose cascade from segments using bottom-to-top nesting.
|
|
1740
|
+
* Processes segments in reverse order, building nested payloads.
|
|
1741
|
+
*
|
|
1742
|
+
* @param segments - Classified segments from classifyIntoSegments()
|
|
1743
|
+
* @param ueaAddress - UEA address
|
|
1744
|
+
* @returns Final MultiCall[] to execute as the initial UEA multicall
|
|
1745
|
+
*/
|
|
1746
|
+
composeCascade(segments, ueaAddress, ueaBalance, ueaNonce) {
|
|
1747
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
1748
|
+
let accumulatedPushMulticalls = [];
|
|
1749
|
+
const gatewayPcAddress = this.getUniversalGatewayPCAddress();
|
|
1750
|
+
// Compute per-outbound nativeValueForGas from UEA balance
|
|
1751
|
+
// Each outbound segment needs native value for the gas swap on the destination chain.
|
|
1752
|
+
// The contract refunds excess, so over-allocating is safe.
|
|
1753
|
+
const numOutbounds = segments.filter(s => s.type !== 'PUSH_EXECUTION').length;
|
|
1754
|
+
const CASCADE_GAS_RESERVE = BigInt(3e18); // 3 PC reserve for gas costs
|
|
1755
|
+
let perOutboundNativeValue;
|
|
1756
|
+
if (ueaBalance && numOutbounds > 0 && ueaBalance > CASCADE_GAS_RESERVE) {
|
|
1757
|
+
perOutboundNativeValue = (ueaBalance - CASCADE_GAS_RESERVE) / BigInt(numOutbounds);
|
|
1758
|
+
}
|
|
1759
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1760
|
+
const segment = segments[i];
|
|
1761
|
+
switch (segment.type) {
|
|
1762
|
+
case 'PUSH_EXECUTION': {
|
|
1763
|
+
// Prepend Push Chain multicalls to accumulated
|
|
1764
|
+
accumulatedPushMulticalls = [
|
|
1765
|
+
...(segment.mergedPushMulticalls || []),
|
|
1766
|
+
...accumulatedPushMulticalls,
|
|
1767
|
+
];
|
|
1768
|
+
break;
|
|
1769
|
+
}
|
|
1770
|
+
case 'OUTBOUND_TO_CEA': {
|
|
1771
|
+
const firstHop = segment.hops[0];
|
|
1772
|
+
const isSvmSegment = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isSvmTarget) === true;
|
|
1773
|
+
let outboundPayload;
|
|
1774
|
+
let targetForOutbound;
|
|
1775
|
+
const isMigration = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isMigration) === true;
|
|
1776
|
+
if (isSvmSegment) {
|
|
1777
|
+
// SVM: use the pre-built SVM payload from the hop descriptor
|
|
1778
|
+
outboundPayload = (_a = firstHop.svmPayload) !== null && _a !== void 0 ? _a : '0x';
|
|
1779
|
+
// For SVM, target is the recipient/program from the hop params
|
|
1780
|
+
const svmTarget = firstHop.params.to;
|
|
1781
|
+
targetForOutbound = (_c = (_b = firstHop.params.svmExecute) === null || _b === void 0 ? void 0 : _b.targetProgram) !== null && _c !== void 0 ? _c : svmTarget.address;
|
|
1782
|
+
}
|
|
1783
|
+
else if (isMigration) {
|
|
1784
|
+
// Migration: use raw 4-byte MIGRATION_SELECTOR, no multicall wrapping
|
|
1785
|
+
outboundPayload = (0, payload_builders_1.buildMigrationPayload)();
|
|
1786
|
+
targetForOutbound = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
// EVM: build CEA payload from merged multicalls
|
|
1790
|
+
outboundPayload = (0, payload_builders_1.buildCeaMulticallPayload)(segment.mergedCeaMulticalls || []);
|
|
1791
|
+
// Get CEA address from the first hop
|
|
1792
|
+
targetForOutbound = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
|
|
1793
|
+
}
|
|
1794
|
+
// Build outbound request
|
|
1795
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetForOutbound, segment.prc20Token || selectors_1.ZERO_ADDRESS, segment.totalBurnAmount || BigInt(0), (_d = segment.gasLimit) !== null && _d !== void 0 ? _d : BigInt(0), outboundPayload, ueaAddress);
|
|
1796
|
+
// Build approval + outbound multicalls
|
|
1797
|
+
const segGasFee = segment.gasFee || BigInt(0);
|
|
1798
|
+
const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
|
|
1799
|
+
prc20Token: segment.prc20Token || selectors_1.ZERO_ADDRESS,
|
|
1800
|
+
gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
|
|
1801
|
+
burnAmount: segment.totalBurnAmount || BigInt(0),
|
|
1802
|
+
gasFee: segGasFee,
|
|
1803
|
+
nativeValueForGas: perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : segGasFee * BigInt(1000),
|
|
1804
|
+
gatewayPcAddress,
|
|
1805
|
+
outboundRequest: outboundReq,
|
|
1806
|
+
});
|
|
1807
|
+
// Prepend to accumulated
|
|
1808
|
+
accumulatedPushMulticalls = [
|
|
1809
|
+
...outboundMulticalls,
|
|
1810
|
+
...accumulatedPushMulticalls,
|
|
1811
|
+
];
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
case 'INBOUND_FROM_CEA': {
|
|
1815
|
+
// The accumulated multicalls = what runs on Push Chain AFTER inbound arrives.
|
|
1816
|
+
// Wrap in UniversalPayload struct with correct UEA nonce for the relay.
|
|
1817
|
+
// Build push multicalls from this hop's own data (e.g., counter.increment())
|
|
1818
|
+
// This is the Route 3 hop's payload that executes on Push Chain after inbound.
|
|
1819
|
+
const hop0 = segment.hops[0];
|
|
1820
|
+
if ((_e = hop0 === null || hop0 === void 0 ? void 0 : hop0.params) === null || _e === void 0 ? void 0 : _e.data) {
|
|
1821
|
+
const hopPushMulticalls = (0, payload_builders_1.buildExecuteMulticall)({
|
|
1822
|
+
execute: {
|
|
1823
|
+
to: hop0.params.to,
|
|
1824
|
+
value: hop0.params.value,
|
|
1825
|
+
data: hop0.params.data,
|
|
1826
|
+
},
|
|
1827
|
+
ueaAddress,
|
|
1828
|
+
});
|
|
1829
|
+
// Prepend the Route 3's own push calls before subsequent hops
|
|
1830
|
+
accumulatedPushMulticalls = [
|
|
1831
|
+
...hopPushMulticalls,
|
|
1832
|
+
...accumulatedPushMulticalls,
|
|
1833
|
+
];
|
|
1834
|
+
}
|
|
1835
|
+
let intermediatePayload = '0x';
|
|
1836
|
+
if (accumulatedPushMulticalls.length > 0) {
|
|
1837
|
+
const multicallPayload = this._buildMulticallPayloadData(ueaAddress, accumulatedPushMulticalls);
|
|
1838
|
+
// +1: the outbound tx consumes one nonce via execute()
|
|
1839
|
+
intermediatePayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: (ueaNonce !== null && ueaNonce !== void 0 ? ueaNonce : BigInt(0)) + BigInt(1) });
|
|
1840
|
+
}
|
|
1841
|
+
const hop = segment.hops[0];
|
|
1842
|
+
const sourceChain = hop.sourceChain;
|
|
1843
|
+
const ceaAddress = hop.ceaAddress || ueaAddress;
|
|
1844
|
+
// SVM chains: build SVM CPI payload instead of EVM CEA multicall
|
|
1845
|
+
if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
|
|
1846
|
+
const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
|
|
1847
|
+
if (!lockerContract) {
|
|
1848
|
+
throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
|
|
1849
|
+
}
|
|
1850
|
+
const programPk = new web3_js_1.PublicKey(lockerContract);
|
|
1851
|
+
const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
|
|
1852
|
+
let drainAmount = BigInt(0);
|
|
1853
|
+
let tokenMintHex;
|
|
1854
|
+
const params = hop.params;
|
|
1855
|
+
if (((_f = params.funds) === null || _f === void 0 ? void 0 : _f.amount) && params.funds.amount > BigInt(0)) {
|
|
1856
|
+
drainAmount = params.funds.amount;
|
|
1857
|
+
const token = params.funds.token;
|
|
1858
|
+
if (token && token.address) {
|
|
1859
|
+
const mintPk = new web3_js_1.PublicKey(token.address);
|
|
1860
|
+
tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
1864
|
+
drainAmount = params.value;
|
|
1865
|
+
}
|
|
1866
|
+
// Derive CEA PDA as revert recipient
|
|
1867
|
+
const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex');
|
|
1868
|
+
const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
|
|
1869
|
+
const ceaPdaHex = ('0x' + Buffer.from(ceaPda.toBytes()).toString('hex'));
|
|
1870
|
+
// Build SVM payload with intermediate Push Chain payload embedded
|
|
1871
|
+
const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
|
|
1872
|
+
gatewayProgramHex,
|
|
1873
|
+
drainAmount,
|
|
1874
|
+
tokenMintHex,
|
|
1875
|
+
revertRecipientHex: ceaPdaHex,
|
|
1876
|
+
extraPayload: intermediatePayload !== '0x'
|
|
1877
|
+
? new Uint8Array(Buffer.from(intermediatePayload.slice(2), 'hex'))
|
|
1878
|
+
: undefined,
|
|
1879
|
+
});
|
|
1880
|
+
const burnAmount = BigInt(1);
|
|
1881
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, segment.prc20Token || this.getNativePRC20ForChain(sourceChain), burnAmount, (_g = segment.gasLimit) !== null && _g !== void 0 ? _g : BigInt(0), svmPayload, ueaAddress);
|
|
1882
|
+
const inboundGasFee = segment.gasFee || BigInt(0);
|
|
1883
|
+
const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
|
|
1884
|
+
prc20Token: segment.prc20Token || this.getNativePRC20ForChain(sourceChain),
|
|
1885
|
+
gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
|
|
1886
|
+
burnAmount,
|
|
1887
|
+
gasFee: inboundGasFee,
|
|
1888
|
+
nativeValueForGas: perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : inboundGasFee * BigInt(1000),
|
|
1889
|
+
gatewayPcAddress,
|
|
1890
|
+
outboundRequest: outboundReq,
|
|
1891
|
+
});
|
|
1892
|
+
accumulatedPushMulticalls = [...outboundMulticalls];
|
|
1893
|
+
break;
|
|
1894
|
+
}
|
|
1895
|
+
// EVM path: Build CEA multicall: [approve?, sendUniversalTxFromCEA(payload)]
|
|
1896
|
+
const ceaMulticalls = [];
|
|
1897
|
+
// Add hop's own CEA operations if any
|
|
1898
|
+
// (e.g., approve + swap before bridging back)
|
|
1899
|
+
if (hop.ceaMulticalls && hop.ceaMulticalls.length > 0) {
|
|
1900
|
+
ceaMulticalls.push(...hop.ceaMulticalls);
|
|
1901
|
+
}
|
|
1902
|
+
// Determine token/amount for inbound
|
|
1903
|
+
let tokenAddress = selectors_1.ZERO_ADDRESS;
|
|
1904
|
+
let amount = BigInt(0);
|
|
1905
|
+
const params = hop.params;
|
|
1906
|
+
if ((_h = params.funds) === null || _h === void 0 ? void 0 : _h.amount) {
|
|
1907
|
+
const token = params.funds.token;
|
|
1908
|
+
if (token) {
|
|
1909
|
+
if (token.mechanism === 'native') {
|
|
1910
|
+
amount = params.funds.amount;
|
|
1911
|
+
}
|
|
1912
|
+
else {
|
|
1913
|
+
tokenAddress = token.address;
|
|
1914
|
+
amount = params.funds.amount;
|
|
1915
|
+
// Add approve for ERC20 (CEA approves gateway)
|
|
1916
|
+
const gatewayAddr = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
|
|
1917
|
+
if (!gatewayAddr) {
|
|
1918
|
+
throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
|
|
1919
|
+
}
|
|
1920
|
+
const approveData = (0, viem_1.encodeFunctionData)({
|
|
1921
|
+
abi: abi_1.ERC20_EVM,
|
|
1922
|
+
functionName: 'approve',
|
|
1923
|
+
args: [gatewayAddr, amount],
|
|
1924
|
+
});
|
|
1925
|
+
ceaMulticalls.push({
|
|
1926
|
+
to: tokenAddress,
|
|
1927
|
+
value: BigInt(0),
|
|
1928
|
+
data: approveData,
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
1934
|
+
amount = params.value;
|
|
1935
|
+
}
|
|
1936
|
+
// Build sendUniversalTxToUEA self-call on CEA (to=CEA, value=0)
|
|
1937
|
+
const sendCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, intermediatePayload, ceaAddress);
|
|
1938
|
+
ceaMulticalls.push(sendCall);
|
|
1939
|
+
// Wrap CEA multicall in outbound from Push Chain
|
|
1940
|
+
const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
|
|
1941
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, segment.prc20Token || this.getNativePRC20ForChain(sourceChain), segment.totalBurnAmount || BigInt(1), (_j = segment.gasLimit) !== null && _j !== void 0 ? _j : BigInt(0), ceaPayload, ueaAddress);
|
|
1942
|
+
const inboundGasFee = segment.gasFee || BigInt(0);
|
|
1943
|
+
const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
|
|
1944
|
+
prc20Token: segment.prc20Token || this.getNativePRC20ForChain(sourceChain),
|
|
1945
|
+
gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
|
|
1946
|
+
burnAmount: segment.totalBurnAmount || BigInt(1),
|
|
1947
|
+
gasFee: inboundGasFee,
|
|
1948
|
+
nativeValueForGas: perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : inboundGasFee * BigInt(1000),
|
|
1949
|
+
gatewayPcAddress,
|
|
1950
|
+
outboundRequest: outboundReq,
|
|
1951
|
+
});
|
|
1952
|
+
// Reset accumulated -- everything is now inside this outbound
|
|
1953
|
+
accumulatedPushMulticalls = [...outboundMulticalls];
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
return accumulatedPushMulticalls;
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Creates a cascaded transaction builder for nested multi-chain execution.
|
|
1962
|
+
* The cascade composes all hops bottom-to-top into a single Push Chain tx.
|
|
1963
|
+
*
|
|
1964
|
+
* @param preparedTxs - Array of prepared transactions
|
|
1965
|
+
* @returns CascadedTransactionBuilder
|
|
1966
|
+
*/
|
|
1967
|
+
createCascadedBuilder(preparedTxs) {
|
|
1968
|
+
return {
|
|
1969
|
+
thenOn: (nextTx) => this.createCascadedBuilder([...preparedTxs, nextTx]),
|
|
1970
|
+
send: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
1971
|
+
const ueaAddress = this.computeUEAOffchain();
|
|
1972
|
+
// Extract HopDescriptors
|
|
1973
|
+
const hops = preparedTxs.map((tx) => tx._hop);
|
|
1974
|
+
// Classify into segments
|
|
1975
|
+
const segments = this.classifyIntoSegments(hops);
|
|
1976
|
+
// Check if this is a single-hop Route 1 (no composition needed)
|
|
1977
|
+
if (preparedTxs.length === 1 &&
|
|
1978
|
+
preparedTxs[0].route === 'UOA_TO_PUSH') {
|
|
1979
|
+
const response = yield this.executeMultiChain(hops[0].params);
|
|
1980
|
+
const singleRoute1Result = {
|
|
1981
|
+
initialTxHash: response.hash,
|
|
1982
|
+
initialTxResponse: response,
|
|
1983
|
+
hops: [
|
|
1984
|
+
{
|
|
1985
|
+
hopIndex: 0,
|
|
1986
|
+
route: hops[0].route,
|
|
1987
|
+
executionChain: enums_1.CHAIN.PUSH_TESTNET_DONUT,
|
|
1988
|
+
status: 'confirmed',
|
|
1989
|
+
txHash: response.hash,
|
|
1990
|
+
},
|
|
1991
|
+
],
|
|
1992
|
+
hopCount: 1,
|
|
1993
|
+
waitForAll: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
1994
|
+
return ({
|
|
1995
|
+
success: true,
|
|
1996
|
+
hops: [
|
|
1997
|
+
{
|
|
1998
|
+
hopIndex: 0,
|
|
1999
|
+
route: hops[0].route,
|
|
2000
|
+
executionChain: enums_1.CHAIN.PUSH_TESTNET_DONUT,
|
|
2001
|
+
status: 'confirmed',
|
|
2002
|
+
txHash: response.hash,
|
|
2003
|
+
},
|
|
2004
|
+
],
|
|
2005
|
+
});
|
|
2006
|
+
}),
|
|
2007
|
+
wait: (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () { return singleRoute1Result.waitForAll(opts); }),
|
|
2008
|
+
};
|
|
2009
|
+
return singleRoute1Result;
|
|
2010
|
+
}
|
|
2011
|
+
// Check if single-hop Route 2 (just execute directly)
|
|
2012
|
+
if (preparedTxs.length === 1 &&
|
|
2013
|
+
preparedTxs[0].route === 'UOA_TO_CEA') {
|
|
2014
|
+
const response = yield this.executeMultiChain(hops[0].params);
|
|
2015
|
+
const targetChain = hops[0].targetChain || enums_1.CHAIN.PUSH_TESTNET_DONUT;
|
|
2016
|
+
const singleRoute2Result = {
|
|
2017
|
+
initialTxHash: response.hash,
|
|
2018
|
+
initialTxResponse: response,
|
|
2019
|
+
hops: [
|
|
2020
|
+
{
|
|
2021
|
+
hopIndex: 0,
|
|
2022
|
+
route: hops[0].route,
|
|
2023
|
+
executionChain: targetChain,
|
|
2024
|
+
status: 'confirmed',
|
|
2025
|
+
txHash: response.hash,
|
|
2026
|
+
},
|
|
2027
|
+
],
|
|
2028
|
+
hopCount: 1,
|
|
2029
|
+
waitForAll: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
2030
|
+
return ({
|
|
2031
|
+
success: true,
|
|
2032
|
+
hops: [
|
|
2033
|
+
{
|
|
2034
|
+
hopIndex: 0,
|
|
2035
|
+
route: hops[0].route,
|
|
2036
|
+
executionChain: targetChain,
|
|
2037
|
+
status: 'confirmed',
|
|
2038
|
+
txHash: response.hash,
|
|
2039
|
+
},
|
|
2040
|
+
],
|
|
2041
|
+
});
|
|
2042
|
+
}),
|
|
2043
|
+
wait: (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () { return singleRoute2Result.waitForAll(opts); }),
|
|
2044
|
+
};
|
|
2045
|
+
return singleRoute2Result;
|
|
2046
|
+
}
|
|
2047
|
+
// Multi-hop: compose cascade bottom-to-top
|
|
2048
|
+
// Fetch UEA balance + nonce so composeCascade can allocate native value and build inbound payloads
|
|
2049
|
+
const ueaBalance = yield this.pushClient.getBalance(ueaAddress);
|
|
2050
|
+
const ueaCodeCascade = yield this.pushClient.publicClient.getCode({ address: ueaAddress });
|
|
2051
|
+
const ueaNonceCascade = ueaCodeCascade !== undefined ? yield this.getUEANonce(ueaAddress) : BigInt(0);
|
|
2052
|
+
const composedMulticalls = this.composeCascade(segments, ueaAddress, ueaBalance, ueaNonceCascade);
|
|
2053
|
+
// Execute the composed multicall as a single Push Chain tx
|
|
2054
|
+
const executeParams = {
|
|
2055
|
+
to: ueaAddress,
|
|
2056
|
+
data: composedMulticalls,
|
|
2057
|
+
};
|
|
2058
|
+
const response = yield this.execute(executeParams);
|
|
2059
|
+
// Build hop info for tracking
|
|
2060
|
+
const hopInfos = hops.map((hop, index) => ({
|
|
2061
|
+
hopIndex: index,
|
|
2062
|
+
route: hop.route,
|
|
2063
|
+
executionChain: hop.targetChain || hop.sourceChain || enums_1.CHAIN.PUSH_TESTNET_DONUT,
|
|
2064
|
+
status: 'pending',
|
|
2065
|
+
}));
|
|
2066
|
+
// Mark first hop as submitted
|
|
2067
|
+
if (hopInfos.length > 0) {
|
|
2068
|
+
hopInfos[0].status = 'submitted';
|
|
2069
|
+
hopInfos[0].txHash = response.hash;
|
|
2070
|
+
}
|
|
2071
|
+
const cascadeResponse = {
|
|
2072
|
+
initialTxHash: response.hash,
|
|
2073
|
+
initialTxResponse: response,
|
|
2074
|
+
hops: hopInfos,
|
|
2075
|
+
hopCount: hops.length,
|
|
2076
|
+
waitForAll: (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
2077
|
+
var _a;
|
|
2078
|
+
const { pollingIntervalMs = 10000, timeout = 300000, progressHook: cascadeProgressHook, } = opts || {};
|
|
2079
|
+
const startTime = Date.now();
|
|
2080
|
+
try {
|
|
2081
|
+
// 1. Wait for initial Push Chain tx confirmation
|
|
2082
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2083
|
+
hopIndex: 0,
|
|
2084
|
+
route: ((_a = hopInfos[0]) === null || _a === void 0 ? void 0 : _a.route) || 'UOA_TO_PUSH',
|
|
2085
|
+
chain: enums_1.CHAIN.PUSH_TESTNET_DONUT,
|
|
2086
|
+
status: 'waiting',
|
|
2087
|
+
elapsed: Date.now() - startTime,
|
|
2088
|
+
});
|
|
2089
|
+
yield response.wait();
|
|
2090
|
+
// Mark all Push Chain (Route 1) hops as confirmed
|
|
2091
|
+
for (const hop of hopInfos) {
|
|
2092
|
+
if (hop.route === 'UOA_TO_PUSH') {
|
|
2093
|
+
hop.status = 'confirmed';
|
|
2094
|
+
hop.txHash = response.hash;
|
|
2095
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2096
|
+
hopIndex: hop.hopIndex,
|
|
2097
|
+
route: hop.route,
|
|
2098
|
+
chain: enums_1.CHAIN.PUSH_TESTNET_DONUT,
|
|
2099
|
+
status: 'confirmed',
|
|
2100
|
+
txHash: response.hash,
|
|
2101
|
+
elapsed: Date.now() - startTime,
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
// 2. Track outbound hops (Route 2: UOA_TO_CEA)
|
|
2106
|
+
// Hops after a CEA_TO_PUSH are "child outbounds" — they execute inside
|
|
2107
|
+
// the inbound payload on Push Chain and live under a DIFFERENT utx_id
|
|
2108
|
+
// (the inbound UTX, not the parent). We can't track them via the parent
|
|
2109
|
+
// utx_id polling, so we auto-confirm them.
|
|
2110
|
+
const ceaToPushIndex = hopInfos.findIndex((h) => h.route === 'CEA_TO_PUSH');
|
|
2111
|
+
const outboundHops = hopInfos.filter((h, i) => {
|
|
2112
|
+
if (h.route !== 'UOA_TO_CEA')
|
|
2113
|
+
return false;
|
|
2114
|
+
// Child outbounds (after CEA_TO_PUSH) live under the inbound UTX,
|
|
2115
|
+
// not the parent — auto-confirm since we can't poll them here.
|
|
2116
|
+
if (ceaToPushIndex >= 0 && i > ceaToPushIndex) {
|
|
2117
|
+
h.status = 'confirmed';
|
|
2118
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2119
|
+
hopIndex: h.hopIndex,
|
|
2120
|
+
route: h.route,
|
|
2121
|
+
chain: h.executionChain,
|
|
2122
|
+
status: 'confirmed',
|
|
2123
|
+
elapsed: Date.now() - startTime,
|
|
2124
|
+
});
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
2127
|
+
return true;
|
|
2128
|
+
});
|
|
2129
|
+
if (outboundHops.length > 0) {
|
|
2130
|
+
if (outboundHops.length === 1) {
|
|
2131
|
+
// Single direct outbound hop: use the existing V1-based tracking
|
|
2132
|
+
const hop = outboundHops[0];
|
|
2133
|
+
const remainingTimeout = timeout - (Date.now() - startTime);
|
|
2134
|
+
if (remainingTimeout <= 0) {
|
|
2135
|
+
hop.status = 'failed';
|
|
2136
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2137
|
+
hopIndex: hop.hopIndex,
|
|
2138
|
+
route: hop.route,
|
|
2139
|
+
chain: hop.executionChain,
|
|
2140
|
+
status: 'timeout',
|
|
2141
|
+
elapsed: Date.now() - startTime,
|
|
2142
|
+
});
|
|
2143
|
+
return { success: false, hops: hopInfos, failedAt: hop.hopIndex };
|
|
2144
|
+
}
|
|
2145
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2146
|
+
hopIndex: hop.hopIndex,
|
|
2147
|
+
route: hop.route,
|
|
2148
|
+
chain: hop.executionChain,
|
|
2149
|
+
status: 'polling',
|
|
2150
|
+
elapsed: Date.now() - startTime,
|
|
2151
|
+
});
|
|
2152
|
+
try {
|
|
2153
|
+
const outboundDetails = yield this.waitForOutboundTx(response.hash, {
|
|
2154
|
+
initialWaitMs: Math.min(60000, remainingTimeout),
|
|
2155
|
+
pollingIntervalMs,
|
|
2156
|
+
timeout: remainingTimeout,
|
|
2157
|
+
progressHook: (event) => {
|
|
2158
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2159
|
+
hopIndex: hop.hopIndex,
|
|
2160
|
+
route: hop.route,
|
|
2161
|
+
chain: hop.executionChain,
|
|
2162
|
+
status: event.status,
|
|
2163
|
+
elapsed: Date.now() - startTime,
|
|
2164
|
+
});
|
|
2165
|
+
},
|
|
2166
|
+
});
|
|
2167
|
+
hop.status = 'confirmed';
|
|
2168
|
+
hop.txHash = outboundDetails.externalTxHash;
|
|
2169
|
+
hop.outboundDetails = outboundDetails;
|
|
2170
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2171
|
+
hopIndex: hop.hopIndex,
|
|
2172
|
+
route: hop.route,
|
|
2173
|
+
chain: hop.executionChain,
|
|
2174
|
+
status: 'confirmed',
|
|
2175
|
+
txHash: outboundDetails.externalTxHash,
|
|
2176
|
+
elapsed: Date.now() - startTime,
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
catch (err) {
|
|
2180
|
+
hop.status = 'failed';
|
|
2181
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2182
|
+
hopIndex: hop.hopIndex,
|
|
2183
|
+
route: hop.route,
|
|
2184
|
+
chain: hop.executionChain,
|
|
2185
|
+
status: 'failed',
|
|
2186
|
+
elapsed: Date.now() - startTime,
|
|
2187
|
+
});
|
|
2188
|
+
return { success: false, hops: hopInfos, failedAt: hop.hopIndex };
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
else {
|
|
2192
|
+
// Multiple outbound hops: use V2 API which returns outboundTx[]
|
|
2193
|
+
const allOutboundDetails = yield this.waitForAllOutboundTxsV2(response.hash, outboundHops, {
|
|
2194
|
+
initialWaitMs: Math.min(60000, timeout - (Date.now() - startTime)),
|
|
2195
|
+
pollingIntervalMs,
|
|
2196
|
+
timeout: timeout - (Date.now() - startTime),
|
|
2197
|
+
progressHook: (event) => {
|
|
2198
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2199
|
+
hopIndex: event.hopIndex,
|
|
2200
|
+
route: event.route,
|
|
2201
|
+
chain: event.chain,
|
|
2202
|
+
status: event.status,
|
|
2203
|
+
txHash: event.txHash,
|
|
2204
|
+
elapsed: Date.now() - startTime,
|
|
2205
|
+
});
|
|
2206
|
+
},
|
|
2207
|
+
});
|
|
2208
|
+
if (!allOutboundDetails.success) {
|
|
2209
|
+
return {
|
|
2210
|
+
success: false,
|
|
2211
|
+
hops: hopInfos,
|
|
2212
|
+
failedAt: allOutboundDetails.failedAt,
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
// 3. Route 3 (CEA_TO_PUSH) tracking - mark as submitted
|
|
2218
|
+
const inboundHops = hopInfos.filter((h) => h.route === 'CEA_TO_PUSH');
|
|
2219
|
+
for (const inboundHop of inboundHops) {
|
|
2220
|
+
inboundHop.status = 'submitted';
|
|
2221
|
+
cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
|
|
2222
|
+
hopIndex: inboundHop.hopIndex,
|
|
2223
|
+
route: inboundHop.route,
|
|
2224
|
+
chain: inboundHop.executionChain,
|
|
2225
|
+
status: 'waiting',
|
|
2226
|
+
elapsed: Date.now() - startTime,
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
return { success: true, hops: hopInfos };
|
|
2230
|
+
}
|
|
2231
|
+
catch (err) {
|
|
2232
|
+
const failedIdx = hopInfos.findIndex((h) => h.status !== 'confirmed');
|
|
2233
|
+
return {
|
|
2234
|
+
success: false,
|
|
2235
|
+
hops: hopInfos,
|
|
2236
|
+
failedAt: failedIdx >= 0 ? failedIdx : 0,
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
}),
|
|
2240
|
+
wait: (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () { return cascadeResponse.waitForAll(opts); }),
|
|
2241
|
+
};
|
|
2242
|
+
return cascadeResponse;
|
|
2243
|
+
}),
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* @deprecated Use createCascadedBuilder instead.
|
|
2248
|
+
* Creates a chained transaction builder for sequential multi-chain execution.
|
|
2249
|
+
*
|
|
2250
|
+
* @param transactions - Array of transactions to execute in sequence
|
|
2251
|
+
* @returns ChainedTransactionBuilder
|
|
2252
|
+
*/
|
|
2253
|
+
createChainedBuilder(transactions) {
|
|
2254
|
+
return {
|
|
2255
|
+
thenOn: (nextTx) => this.createChainedBuilder([...transactions, nextTx]),
|
|
2256
|
+
send: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
2257
|
+
const responses = [];
|
|
2258
|
+
for (let i = 0; i < transactions.length; i++) {
|
|
2259
|
+
const response = yield this.execute(transactions[i]);
|
|
2260
|
+
response.hopIndex = i;
|
|
2261
|
+
if (i > 0 && responses[i - 1]) {
|
|
2262
|
+
response.parentTxHash = responses[i - 1].hash;
|
|
2263
|
+
responses[i - 1].childTxHash = response.hash;
|
|
2264
|
+
}
|
|
2265
|
+
responses.push(response);
|
|
2266
|
+
}
|
|
2267
|
+
return {
|
|
2268
|
+
transactions: responses,
|
|
2269
|
+
chains: responses.map((r) => ({
|
|
2270
|
+
chain: r.chain || enums_1.CHAIN.PUSH_TESTNET_DONUT,
|
|
2271
|
+
hash: r.hash,
|
|
2272
|
+
blockNumber: r.blockNumber,
|
|
2273
|
+
status: 'confirmed',
|
|
2274
|
+
})),
|
|
2275
|
+
};
|
|
2276
|
+
}),
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Route 2: Execute outbound transaction from Push Chain to external CEA
|
|
2281
|
+
*
|
|
2282
|
+
* This method builds a multicall that executes on Push Chain (from UEA context):
|
|
2283
|
+
* 1. Approves the gateway to spend PRC-20 tokens (if needed)
|
|
2284
|
+
* 2. Calls sendUniversalTxOutbound on UniversalGatewayPC precompile
|
|
2285
|
+
*
|
|
2286
|
+
* The multicall is executed through the normal execute() flow which handles
|
|
2287
|
+
* fee-locking on the origin chain and signature verification.
|
|
2288
|
+
*
|
|
2289
|
+
* @param params - Universal execution parameters with ChainTarget
|
|
2290
|
+
* @returns UniversalTxResponse
|
|
2291
|
+
*/
|
|
2292
|
+
executeUoaToCea(params) {
|
|
2293
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
2294
|
+
var _a, _b, _c, _d, _e;
|
|
2295
|
+
const target = params.to;
|
|
2296
|
+
const targetChain = target.chain;
|
|
2297
|
+
const targetAddress = target.address;
|
|
2298
|
+
const isSvm = (0, payload_builders_1.isSvmChain)(targetChain);
|
|
2299
|
+
// Validate target address based on VM type
|
|
2300
|
+
if (isSvm) {
|
|
2301
|
+
// SVM: 32-byte hex address
|
|
2302
|
+
if (!(0, payload_builders_1.isValidSolanaHexAddress)(targetAddress)) {
|
|
2303
|
+
throw new Error(`Invalid Solana address: ${targetAddress}. ` +
|
|
2304
|
+
`Expected 0x + 64 hex chars (32 bytes).`);
|
|
2305
|
+
}
|
|
2306
|
+
const ZERO_32 = ('0x' + '0'.repeat(64));
|
|
2307
|
+
if (targetAddress.toLowerCase() === ZERO_32.toLowerCase()) {
|
|
2308
|
+
throw new Error(`Cannot send to zero address on Solana. ` +
|
|
2309
|
+
`This would result in permanent loss of funds.`);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
else {
|
|
2313
|
+
// EVM: 20-byte hex address
|
|
2314
|
+
// Zero address is allowed for multicall (data is array) — the actual targets are in the data entries.
|
|
2315
|
+
const isMulticall = Array.isArray(params.data);
|
|
2316
|
+
if (targetAddress.toLowerCase() === selectors_1.ZERO_ADDRESS.toLowerCase() && !isMulticall) {
|
|
2317
|
+
throw new Error(`Cannot send to zero address (0x0000...0000). ` +
|
|
2318
|
+
`This would result in permanent loss of funds.`);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
// Validate chain supports outbound operations
|
|
2322
|
+
if (!(0, cea_utils_1.chainSupportsOutbound)(targetChain)) {
|
|
2323
|
+
throw new Error(`Chain ${targetChain} does not support outbound operations. ` +
|
|
2324
|
+
`Supported chains: BNB_TESTNET, ETHEREUM_SEPOLIA, SOLANA_DEVNET, etc.`);
|
|
2325
|
+
}
|
|
2326
|
+
// Branch based on VM type
|
|
2327
|
+
if (isSvm) {
|
|
2328
|
+
return this.executeUoaToCeaSvm(params, target);
|
|
2329
|
+
}
|
|
2330
|
+
// ===== EVM path (existing logic) =====
|
|
2331
|
+
// Get UEA address
|
|
2332
|
+
const ueaAddress = this.computeUEAOffchain();
|
|
2333
|
+
this.printLog(`executeUoaToCea — target chain: ${targetChain}, target address: ${targetAddress}, UEA: ${ueaAddress}`);
|
|
2334
|
+
// Get CEA address for this UEA on target chain
|
|
2335
|
+
const { cea: ceaAddress, isDeployed: ceaDeployed } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_a = this.rpcUrls[targetChain]) === null || _a === void 0 ? void 0 : _a[0]);
|
|
2336
|
+
this.printLog(`executeUoaToCea — CEA address: ${ceaAddress}, deployed: ${ceaDeployed}`);
|
|
2337
|
+
// Migration path: raw MIGRATION_SELECTOR payload, no multicall wrapping
|
|
2338
|
+
let ceaPayload;
|
|
2339
|
+
let prc20Token = selectors_1.ZERO_ADDRESS;
|
|
2340
|
+
let burnAmount = BigInt(0);
|
|
2341
|
+
if (params.migration) {
|
|
2342
|
+
ceaPayload = (0, payload_builders_1.buildMigrationPayload)();
|
|
2343
|
+
prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
2344
|
+
burnAmount = BigInt(0); // Migration is logic-only — no funds. CEA rejects msg.value != 0.
|
|
2345
|
+
this.printLog(`executeUoaToCea — MIGRATION: using raw MIGRATION_SELECTOR payload (${ceaPayload}), native PRC-20 ${prc20Token}`);
|
|
2346
|
+
}
|
|
2347
|
+
else {
|
|
2348
|
+
// Build multicall for CEA execution on target chain
|
|
2349
|
+
const ceaMulticalls = [];
|
|
2350
|
+
// If there's data to execute on target
|
|
2351
|
+
if (params.data) {
|
|
2352
|
+
if (Array.isArray(params.data)) {
|
|
2353
|
+
// User provided explicit multicall array
|
|
2354
|
+
ceaMulticalls.push(...params.data);
|
|
2355
|
+
}
|
|
2356
|
+
else {
|
|
2357
|
+
// When ERC-20 funds are provided with a single payload, auto-prepend a
|
|
2358
|
+
// transfer() call so the tokens minted to the CEA are forwarded to the
|
|
2359
|
+
// target address. This mirrors the Route 1 behavior in buildExecuteMulticall.
|
|
2360
|
+
if ((_b = params.funds) === null || _b === void 0 ? void 0 : _b.amount) {
|
|
2361
|
+
const token = params.funds.token;
|
|
2362
|
+
if (token && token.mechanism !== 'native') {
|
|
2363
|
+
const erc20Transfer = (0, viem_1.encodeFunctionData)({
|
|
2364
|
+
abi: abi_1.ERC20_EVM,
|
|
2365
|
+
functionName: 'transfer',
|
|
2366
|
+
args: [targetAddress, params.funds.amount],
|
|
2367
|
+
});
|
|
2368
|
+
ceaMulticalls.push({
|
|
2369
|
+
to: token.address,
|
|
2370
|
+
value: BigInt(0),
|
|
2371
|
+
data: erc20Transfer,
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
// Single call with data. Forward native value (if any) so the target
|
|
2376
|
+
// contract receives it alongside the payload call. The vault deposits
|
|
2377
|
+
// native value to the CEA, and the multicall forwards it to the target.
|
|
2378
|
+
ceaMulticalls.push({
|
|
2379
|
+
to: targetAddress,
|
|
2380
|
+
value: (_c = params.value) !== null && _c !== void 0 ? _c : BigInt(0),
|
|
2381
|
+
data: params.data,
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
else if (params.value) {
|
|
2386
|
+
// Native value transfer only.
|
|
2387
|
+
// If sending to the CEA itself, skip the multicall — the gateway deposits native
|
|
2388
|
+
// value directly to CEA. A self-call with value would revert (CEA._handleMulticall
|
|
2389
|
+
// rejects value-bearing self-calls).
|
|
2390
|
+
if (targetAddress.toLowerCase() !== ceaAddress.toLowerCase()) {
|
|
2391
|
+
ceaMulticalls.push({
|
|
2392
|
+
to: targetAddress,
|
|
2393
|
+
value: params.value,
|
|
2394
|
+
data: '0x',
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
// Build CEA multicall payload (this is what gets executed on the external chain)
|
|
2399
|
+
ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
|
|
2400
|
+
// Determine token to burn on Push Chain
|
|
2401
|
+
// NOTE: Even for PAYLOAD-only (no value), we need a valid PRC-20 token to:
|
|
2402
|
+
// 1. Look up the target chain namespace in the gateway
|
|
2403
|
+
// 2. Query and pay gas fees for the relay
|
|
2404
|
+
if ((_d = params.funds) === null || _d === void 0 ? void 0 : _d.amount) {
|
|
2405
|
+
// User explicitly specified funds with token
|
|
2406
|
+
const token = params.funds.token;
|
|
2407
|
+
if (token) {
|
|
2408
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
2409
|
+
burnAmount = params.funds.amount;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
2413
|
+
// Native value transfer: auto-select the PRC-20 token for target chain
|
|
2414
|
+
prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
2415
|
+
burnAmount = params.value;
|
|
2416
|
+
this.printLog(`executeUoaToCea — auto-selected native PRC-20 ${prc20Token} for chain ${targetChain}, amount: ${burnAmount.toString()}`);
|
|
2417
|
+
}
|
|
2418
|
+
else if (params.data) {
|
|
2419
|
+
// PAYLOAD-only (no value transfer): still need native token for chain namespace + gas fees
|
|
2420
|
+
prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
2421
|
+
burnAmount = BigInt(0);
|
|
2422
|
+
this.printLog(`executeUoaToCea — PAYLOAD-only: using native PRC-20 ${prc20Token} for chain ${targetChain} with zero burn amount`);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
// Build outbound request struct for the gateway
|
|
2426
|
+
// NOTE: `target` is a LEGACY/DUMMY parameter for contract compatibility.
|
|
2427
|
+
// The deployed UniversalGatewayPC still expects this field, but the relay does NOT use it
|
|
2428
|
+
// to determine the actual destination. The relay determines destination from the PRC-20 token's
|
|
2429
|
+
// SOURCE_CHAIN_NAMESPACE. We pass the CEA address as a non-zero placeholder.
|
|
2430
|
+
// This field will be removed in future contract upgrades.
|
|
2431
|
+
const targetBytes = ceaAddress; // Dummy value - any non-zero address works
|
|
2432
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_e = params.gasLimit) !== null && _e !== void 0 ? _e : BigInt(0), ceaPayload, ueaAddress // revert recipient is the UEA
|
|
2433
|
+
);
|
|
2434
|
+
this.printLog(`executeUoaToCea — outbound request: ${JSON.stringify({
|
|
2435
|
+
target: outboundReq.target,
|
|
2436
|
+
token: outboundReq.token,
|
|
2437
|
+
amount: outboundReq.amount.toString(),
|
|
2438
|
+
gasLimit: outboundReq.gasLimit.toString(),
|
|
2439
|
+
payloadLength: outboundReq.payload.length,
|
|
2440
|
+
revertRecipient: outboundReq.revertRecipient,
|
|
2441
|
+
}, null, 2)}`);
|
|
2442
|
+
// Get UniversalGatewayPC address
|
|
2443
|
+
const gatewayPcAddress = this.getUniversalGatewayPCAddress();
|
|
2444
|
+
// Pre-fetch UEA status early — balance is needed for gas value calculation
|
|
2445
|
+
const [ueaCode, ueaBalance] = yield Promise.all([
|
|
2446
|
+
this.pushClient.publicClient.getCode({ address: ueaAddress }),
|
|
2447
|
+
this.pushClient.getBalance(ueaAddress),
|
|
2448
|
+
]);
|
|
2449
|
+
const isUEADeployed = ueaCode !== undefined;
|
|
2450
|
+
const ueaNonce = isUEADeployed ? yield this.getUEANonce(ueaAddress) : BigInt(0);
|
|
2451
|
+
// Build the multicall that will execute ON Push Chain from UEA context
|
|
2452
|
+
// This includes: 1) approve PRC-20 (if needed), 2) call sendUniversalTxOutbound
|
|
2453
|
+
const pushChainMulticalls = [];
|
|
2454
|
+
// Query gas fee from UniversalCore contract (needed for approval amount)
|
|
2455
|
+
let gasFee = BigInt(0);
|
|
2456
|
+
let nativeValueForGas = BigInt(0);
|
|
2457
|
+
let gasToken = selectors_1.ZERO_ADDRESS;
|
|
2458
|
+
if (prc20Token !== selectors_1.ZERO_ADDRESS) {
|
|
2459
|
+
try {
|
|
2460
|
+
const result = yield this.queryOutboundGasFee(prc20Token, outboundReq.gasLimit);
|
|
2461
|
+
gasFee = result.gasFee;
|
|
2462
|
+
gasToken = result.gasToken;
|
|
2463
|
+
nativeValueForGas = result.nativeValueForGas;
|
|
2464
|
+
this.printLog(`executeUoaToCea — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}`);
|
|
2465
|
+
}
|
|
2466
|
+
catch (err) {
|
|
2467
|
+
throw new Error(`Failed to query outbound gas fee: ${err}`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
// Adjust nativeValueForGas: the 1Mx multiplier from queryOutboundGasFee produces
|
|
2471
|
+
// a value far too low for the actual WPC/gasToken swap price. Set it to 200 UPC
|
|
2472
|
+
// (capped by balance) — enough for the swap, but avoids draining thin pools.
|
|
2473
|
+
// The contract's swapAndBurnGas does exactOutputSingle and refunds excess PC.
|
|
2474
|
+
const EVM_NATIVE_VALUE_TARGET = BigInt(200e18); // 200 UPC
|
|
2475
|
+
const EVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for tx overhead
|
|
2476
|
+
const currentBalance = yield this.pushClient.getBalance(ueaAddress);
|
|
2477
|
+
let adjustedValue;
|
|
2478
|
+
if (currentBalance > EVM_NATIVE_VALUE_TARGET + EVM_GAS_RESERVE) {
|
|
2479
|
+
// Enough balance: use 200 UPC target
|
|
2480
|
+
adjustedValue = EVM_NATIVE_VALUE_TARGET;
|
|
2481
|
+
}
|
|
2482
|
+
else if (currentBalance > EVM_GAS_RESERVE) {
|
|
2483
|
+
// Low balance: use what's available minus reserve
|
|
2484
|
+
adjustedValue = currentBalance - EVM_GAS_RESERVE;
|
|
2485
|
+
}
|
|
2486
|
+
else {
|
|
2487
|
+
// Very low balance: use original query value as-is
|
|
2488
|
+
adjustedValue = nativeValueForGas;
|
|
2489
|
+
}
|
|
2490
|
+
if (adjustedValue !== nativeValueForGas) {
|
|
2491
|
+
this.printLog(`executeUoaToCea — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} (UEA balance: ${currentBalance.toString()})`);
|
|
2492
|
+
nativeValueForGas = adjustedValue;
|
|
2493
|
+
}
|
|
2494
|
+
// Build outbound multicalls (approve burn + sendUniversalTxOutbound with native value)
|
|
2495
|
+
const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
|
|
2496
|
+
prc20Token,
|
|
2497
|
+
gasToken,
|
|
2498
|
+
burnAmount,
|
|
2499
|
+
gasFee,
|
|
2500
|
+
nativeValueForGas,
|
|
2501
|
+
gatewayPcAddress,
|
|
2502
|
+
outboundRequest: outboundReq,
|
|
2503
|
+
});
|
|
2504
|
+
pushChainMulticalls.push(...outboundMulticalls);
|
|
2505
|
+
this.printLog(`executeUoaToCea — Push Chain multicall has ${pushChainMulticalls.length} operations`);
|
|
2506
|
+
// Execute through the normal execute() flow
|
|
2507
|
+
// This handles fee-locking on origin chain and executes the multicall from UEA context
|
|
2508
|
+
// Sum native values from multicall entries for proper fee calculation
|
|
2509
|
+
const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => { var _a; return sum + ((_a = mc.value) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
|
|
2510
|
+
const executeParams = {
|
|
2511
|
+
to: ueaAddress, // multicall executes from UEA
|
|
2512
|
+
value: multicallNativeValue, // ensures correct requiredFunds calculation
|
|
2513
|
+
data: pushChainMulticalls, // array triggers multicall mode
|
|
2514
|
+
_ueaStatus: {
|
|
2515
|
+
isDeployed: isUEADeployed,
|
|
2516
|
+
nonce: ueaNonce,
|
|
2517
|
+
balance: ueaBalance,
|
|
2518
|
+
},
|
|
2519
|
+
_skipFeeLocking: true, // outbound executes on Push Chain, no external fee locking
|
|
2520
|
+
};
|
|
2521
|
+
const response = yield this.execute(executeParams);
|
|
2522
|
+
// Add chain info to response
|
|
2523
|
+
response.chain = targetChain;
|
|
2524
|
+
response.chainNamespace = this.getChainNamespace(targetChain);
|
|
2525
|
+
return response;
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Route 2 for SVM targets: Outbound from Push Chain to Solana.
|
|
2530
|
+
*
|
|
2531
|
+
* Three cases:
|
|
2532
|
+
* 1. Withdraw SOL: Burn pSOL on Push Chain, recipient gets native SOL
|
|
2533
|
+
* 2. Withdraw SPL: Burn PRC-20 on Push Chain, recipient gets SPL token
|
|
2534
|
+
* 3. Execute (CPI): Burn pSOL + execute CPI on target Solana program
|
|
2535
|
+
*
|
|
2536
|
+
* @param params - Universal execution parameters
|
|
2537
|
+
* @param target - ChainTarget with Solana chain and hex address
|
|
2538
|
+
* @returns UniversalTxResponse
|
|
2539
|
+
*/
|
|
2540
|
+
executeUoaToCeaSvm(params, target) {
|
|
2541
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
2542
|
+
var _a, _b, _c, _d;
|
|
2543
|
+
const targetChain = target.chain;
|
|
2544
|
+
const targetAddress = target.address; // 0x-prefixed, 32 bytes
|
|
2545
|
+
const ueaAddress = this.computeUEAOffchain();
|
|
2546
|
+
const hasSvmExecute = !!params.svmExecute;
|
|
2547
|
+
this.printLog(`executeUoaToCeaSvm — target: ${targetAddress}, chain: ${targetChain}, ` +
|
|
2548
|
+
`hasSvmExecute: ${hasSvmExecute}, value: ${(_b = (_a = params.value) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : '0'}`);
|
|
2549
|
+
// --- Build SVM payload ---
|
|
2550
|
+
let svmPayload = '0x'; // empty for withdraw (SOL/SPL)
|
|
2551
|
+
if (hasSvmExecute) {
|
|
2552
|
+
// Execute case: encode the CPI payload
|
|
2553
|
+
const exec = params.svmExecute;
|
|
2554
|
+
svmPayload = (0, payload_builders_1.encodeSvmExecutePayload)({
|
|
2555
|
+
targetProgram: exec.targetProgram,
|
|
2556
|
+
accounts: exec.accounts,
|
|
2557
|
+
ixData: exec.ixData,
|
|
2558
|
+
instructionId: 2,
|
|
2559
|
+
});
|
|
2560
|
+
this.printLog(`executeUoaToCeaSvm — encoded execute payload: ${(svmPayload.length - 2) / 2} bytes, ` +
|
|
2561
|
+
`${exec.accounts.length} accounts`);
|
|
2562
|
+
}
|
|
2563
|
+
// --- Determine PRC-20 token and burn amount ---
|
|
2564
|
+
let prc20Token = selectors_1.ZERO_ADDRESS;
|
|
2565
|
+
let burnAmount = BigInt(0);
|
|
2566
|
+
if ((_c = params.funds) === null || _c === void 0 ? void 0 : _c.amount) {
|
|
2567
|
+
// User explicitly specified funds with token
|
|
2568
|
+
const token = params.funds.token;
|
|
2569
|
+
if (token) {
|
|
2570
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
2571
|
+
burnAmount = params.funds.amount;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
2575
|
+
// Native value transfer: auto-select pSOL for Solana chains
|
|
2576
|
+
prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
2577
|
+
burnAmount = params.value;
|
|
2578
|
+
this.printLog(`executeUoaToCeaSvm — auto-selected native PRC-20 ${prc20Token} for chain ${targetChain}, amount: ${burnAmount.toString()}`);
|
|
2579
|
+
}
|
|
2580
|
+
else if (hasSvmExecute) {
|
|
2581
|
+
// Execute-only (no value): check if user specified an SPL token context
|
|
2582
|
+
const token = params.funds && params.funds.token;
|
|
2583
|
+
if (token) {
|
|
2584
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
2585
|
+
}
|
|
2586
|
+
else {
|
|
2587
|
+
prc20Token = this.getNativePRC20ForChain(targetChain);
|
|
2588
|
+
}
|
|
2589
|
+
burnAmount = BigInt(0);
|
|
2590
|
+
this.printLog(`executeUoaToCeaSvm — EXECUTE-only: using PRC-20 ${prc20Token} with zero burn amount`);
|
|
2591
|
+
}
|
|
2592
|
+
// --- Determine target bytes ---
|
|
2593
|
+
// For withdraw: target is the Solana recipient pubkey
|
|
2594
|
+
// For execute: target is the target Solana program
|
|
2595
|
+
const targetBytes = hasSvmExecute
|
|
2596
|
+
? params.svmExecute.targetProgram
|
|
2597
|
+
: targetAddress;
|
|
2598
|
+
// --- Build outbound request ---
|
|
2599
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_d = params.gasLimit) !== null && _d !== void 0 ? _d : BigInt(0), svmPayload, ueaAddress // revert recipient is the UEA
|
|
2600
|
+
);
|
|
2601
|
+
this.printLog(`executeUoaToCeaSvm — outbound request: ${JSON.stringify({
|
|
2602
|
+
target: outboundReq.target,
|
|
2603
|
+
token: outboundReq.token,
|
|
2604
|
+
amount: outboundReq.amount.toString(),
|
|
2605
|
+
gasLimit: outboundReq.gasLimit.toString(),
|
|
2606
|
+
payloadLength: (outboundReq.payload.length - 2) / 2,
|
|
2607
|
+
revertRecipient: outboundReq.revertRecipient,
|
|
2608
|
+
}, null, 2)}`);
|
|
2609
|
+
// --- Pre-fetch UEA status early — balance is needed for gas value calculation ---
|
|
2610
|
+
const gatewayPcAddress = this.getUniversalGatewayPCAddress();
|
|
2611
|
+
const [ueaCode, ueaBalance] = yield Promise.all([
|
|
2612
|
+
this.pushClient.publicClient.getCode({ address: ueaAddress }),
|
|
2613
|
+
this.pushClient.getBalance(ueaAddress),
|
|
2614
|
+
]);
|
|
2615
|
+
const isUEADeployed = ueaCode !== undefined;
|
|
2616
|
+
const ueaNonce = isUEADeployed
|
|
2617
|
+
? yield this.getUEANonce(ueaAddress)
|
|
2618
|
+
: BigInt(0);
|
|
2619
|
+
// --- Query gas fee (identical to EVM path) ---
|
|
2620
|
+
let gasFee = BigInt(0);
|
|
2621
|
+
let gasToken = selectors_1.ZERO_ADDRESS;
|
|
2622
|
+
let nativeValueForGas = BigInt(0);
|
|
2623
|
+
if (prc20Token !== selectors_1.ZERO_ADDRESS) {
|
|
2624
|
+
const gasLimit = outboundReq.gasLimit;
|
|
2625
|
+
try {
|
|
2626
|
+
const result = yield this.queryOutboundGasFee(prc20Token, gasLimit);
|
|
2627
|
+
gasFee = result.gasFee;
|
|
2628
|
+
gasToken = result.gasToken;
|
|
2629
|
+
nativeValueForGas = result.nativeValueForGas;
|
|
2630
|
+
// When user omits gasLimit (sent as 0), the contract computes fees using its internal
|
|
2631
|
+
// baseGasLimitByChainNamespace. But the relay reads the stored gasLimit=0 from the
|
|
2632
|
+
// on-chain outbound record and uses it as the Solana compute budget — 0 CU means the
|
|
2633
|
+
// relay cannot execute the tx. Derive the effective limit from gasFee/gasPrice so
|
|
2634
|
+
// the stored record has a non-zero compute budget the relay can use.
|
|
2635
|
+
if (!params.gasLimit && result.gasPrice > BigInt(0)) {
|
|
2636
|
+
outboundReq.gasLimit = result.gasFee / result.gasPrice;
|
|
2637
|
+
this.printLog(`executeUoaToCeaSvm — derived effectiveGasLimit: ${outboundReq.gasLimit} (gasFee=${result.gasFee} / gasPrice=${result.gasPrice})`);
|
|
2638
|
+
}
|
|
2639
|
+
this.printLog(`executeUoaToCeaSvm — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}`);
|
|
2640
|
+
}
|
|
2641
|
+
catch (err) {
|
|
2642
|
+
throw new Error(`Failed to query outbound gas fee: ${err}`);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
// Adjust nativeValueForGas: the 1Mx multiplier from queryOutboundGasFee produces
|
|
2646
|
+
// a value far too low for the actual WPC/gasToken swap price. Set it to 200 UPC
|
|
2647
|
+
// (capped by balance) — enough for the swap, but avoids draining thin pools.
|
|
2648
|
+
// The contract's swapAndBurnGas does exactOutputSingle and refunds excess PC.
|
|
2649
|
+
const SVM_NATIVE_VALUE_TARGET = BigInt(200e18); // 200 UPC
|
|
2650
|
+
const SVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for tx overhead
|
|
2651
|
+
const currentBalance = yield this.pushClient.getBalance(ueaAddress);
|
|
2652
|
+
let adjustedValue;
|
|
2653
|
+
if (currentBalance > SVM_NATIVE_VALUE_TARGET + SVM_GAS_RESERVE) {
|
|
2654
|
+
// Enough balance: use 200 UPC target
|
|
2655
|
+
adjustedValue = SVM_NATIVE_VALUE_TARGET;
|
|
2656
|
+
}
|
|
2657
|
+
else if (currentBalance > SVM_GAS_RESERVE) {
|
|
2658
|
+
// Low balance: use what's available minus reserve
|
|
2659
|
+
adjustedValue = currentBalance - SVM_GAS_RESERVE;
|
|
2660
|
+
}
|
|
2661
|
+
else {
|
|
2662
|
+
// Very low balance: use original query value as-is
|
|
2663
|
+
adjustedValue = nativeValueForGas;
|
|
2664
|
+
}
|
|
2665
|
+
if (adjustedValue !== nativeValueForGas) {
|
|
2666
|
+
this.printLog(`executeUoaToCeaSvm — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} (UEA balance: ${currentBalance.toString()})`);
|
|
2667
|
+
nativeValueForGas = adjustedValue;
|
|
2668
|
+
}
|
|
2669
|
+
// --- Build Push Chain multicalls (approve + sendUniversalTxOutbound) ---
|
|
2670
|
+
// Reuse the same builder as EVM — this part is identical
|
|
2671
|
+
const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
|
|
2672
|
+
prc20Token,
|
|
2673
|
+
gasToken,
|
|
2674
|
+
burnAmount,
|
|
2675
|
+
gasFee,
|
|
2676
|
+
nativeValueForGas,
|
|
2677
|
+
gatewayPcAddress,
|
|
2678
|
+
outboundRequest: outboundReq,
|
|
2679
|
+
});
|
|
2680
|
+
this.printLog(`executeUoaToCeaSvm — Push Chain multicall has ${pushChainMulticalls.length} operations`);
|
|
2681
|
+
// Sum native values from multicall entries for proper fee calculation
|
|
2682
|
+
const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => { var _a; return sum + ((_a = mc.value) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
|
|
2683
|
+
const executeParams = {
|
|
2684
|
+
to: ueaAddress,
|
|
2685
|
+
value: multicallNativeValue, // ensures correct requiredFunds calculation
|
|
2686
|
+
data: pushChainMulticalls,
|
|
2687
|
+
_ueaStatus: {
|
|
2688
|
+
isDeployed: isUEADeployed,
|
|
2689
|
+
nonce: ueaNonce,
|
|
2690
|
+
balance: ueaBalance,
|
|
2691
|
+
},
|
|
2692
|
+
_skipFeeLocking: true, // outbound executes on Push Chain, no external fee locking
|
|
2693
|
+
};
|
|
2694
|
+
const response = yield this.execute(executeParams);
|
|
2695
|
+
// Add chain info to response
|
|
2696
|
+
response.chain = targetChain;
|
|
2697
|
+
response.chainNamespace = this.getChainNamespace(targetChain);
|
|
2698
|
+
return response;
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Route 3: Execute inbound transaction from CEA to Push Chain
|
|
2703
|
+
*
|
|
2704
|
+
* This route instructs CEA on an external chain to call sendUniversalTxFromCEA,
|
|
2705
|
+
* bridging funds/payloads back to Push Chain.
|
|
2706
|
+
*
|
|
2707
|
+
* Flow:
|
|
2708
|
+
* 1. Build multicall for CEA: [approve Gateway (if ERC20), sendUniversalTxFromCEA]
|
|
2709
|
+
* 2. Execute via Route 2 (UOA → CEA) with PAYLOAD-only (CEA uses its own funds)
|
|
2710
|
+
* 3. CEA executes multicall, Gateway locks funds, relayer mints PRC-20 on Push Chain
|
|
2711
|
+
*
|
|
2712
|
+
* @param params - Universal execution parameters with from.chain specified
|
|
2713
|
+
* @returns UniversalTxResponse
|
|
2714
|
+
*/
|
|
2715
|
+
executeCeaToPush(params) {
|
|
2716
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
2717
|
+
var _a, _b, _c, _d, _e;
|
|
2718
|
+
// 1. Validate and extract source chain
|
|
2719
|
+
if (!((_a = params.from) === null || _a === void 0 ? void 0 : _a.chain)) {
|
|
2720
|
+
throw new Error('Route 3 (CEA → Push) requires from.chain to specify the source CEA chain');
|
|
2721
|
+
}
|
|
2722
|
+
const sourceChain = params.from.chain;
|
|
2723
|
+
// SVM chains use a fundamentally different flow (gateway self-call, not CEA multicall)
|
|
2724
|
+
if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
|
|
2725
|
+
return this.executeCeaToPushSvm(params, sourceChain);
|
|
2726
|
+
}
|
|
2727
|
+
// 2. Extract destination on Push Chain
|
|
2728
|
+
// For Route 3, 'to' is a Push Chain address (string), not a ChainTarget
|
|
2729
|
+
const pushDestination = params.to;
|
|
2730
|
+
if (typeof params.to !== 'string') {
|
|
2731
|
+
throw new Error('Route 3 (CEA → Push): to must be a Push Chain address (string), not a ChainTarget');
|
|
2732
|
+
}
|
|
2733
|
+
// 3. Get UEA address (will be recipient on Push Chain from CEA's perspective)
|
|
2734
|
+
const ueaAddress = this.computeUEAOffchain();
|
|
2735
|
+
// 4. Get CEA address on source chain
|
|
2736
|
+
const { cea: ceaAddress, isDeployed: ceaDeployed } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_b = this.rpcUrls[sourceChain]) === null || _b === void 0 ? void 0 : _b[0]);
|
|
2737
|
+
this.printLog(`executeCeaToPush — sourceChain: ${sourceChain}, CEA: ${ceaAddress}, deployed: ${ceaDeployed}`);
|
|
2738
|
+
if (!ceaDeployed) {
|
|
2739
|
+
throw new Error(`CEA not deployed on ${sourceChain}. ` +
|
|
2740
|
+
`Deploy CEA first using Route 2 (UOA → CEA) before using Route 3.`);
|
|
2741
|
+
}
|
|
2742
|
+
// 5. Get UniversalGateway address on source chain
|
|
2743
|
+
const gatewayAddress = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
|
|
2744
|
+
if (!gatewayAddress) {
|
|
2745
|
+
throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
|
|
2746
|
+
}
|
|
2747
|
+
// 6. Build multicall for CEA to execute on source chain (self-calls via sendUniversalTxToUEA)
|
|
2748
|
+
const ceaMulticalls = [];
|
|
2749
|
+
// Determine token and amount for sendUniversalTxToUEA
|
|
2750
|
+
let tokenAddress = selectors_1.ZERO_ADDRESS;
|
|
2751
|
+
let amount = BigInt(0);
|
|
2752
|
+
if ((_c = params.funds) === null || _c === void 0 ? void 0 : _c.amount) {
|
|
2753
|
+
// ERC20 token transfer from CEA
|
|
2754
|
+
const token = params.funds.token;
|
|
2755
|
+
if (token) {
|
|
2756
|
+
if (token.mechanism === 'native') {
|
|
2757
|
+
// Native token (e.g., BNB on BSC)
|
|
2758
|
+
tokenAddress = selectors_1.ZERO_ADDRESS;
|
|
2759
|
+
amount = params.funds.amount;
|
|
2760
|
+
}
|
|
2761
|
+
else {
|
|
2762
|
+
// ERC20 token - need approval for gateway before sendUniversalTxToUEA
|
|
2763
|
+
tokenAddress = token.address;
|
|
2764
|
+
amount = params.funds.amount;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
2769
|
+
// Native value transfer (e.g., BNB, ETH)
|
|
2770
|
+
tokenAddress = selectors_1.ZERO_ADDRESS;
|
|
2771
|
+
amount = params.value;
|
|
2772
|
+
}
|
|
2773
|
+
// bridgeAmount = only the burn amount (what the Vault will actually deposit to CEA).
|
|
2774
|
+
// Previously this included ceaExistingBalance (CEA's pre-existing balance on the
|
|
2775
|
+
// external chain), but that approach is racy: the balance can change between the SDK
|
|
2776
|
+
// query and relay execution, causing sendUniversalTxToUEA to revert with
|
|
2777
|
+
// InsufficientBalance. Pre-existing CEA funds remain parked and can be swept separately.
|
|
2778
|
+
let bridgeAmount = amount;
|
|
2779
|
+
// Note: CEA contract may reject amount=0 in sendUniversalTxToUEA.
|
|
2780
|
+
// Keeping bridgeAmount as-is (0) for payload-only to test precompile behavior.
|
|
2781
|
+
// For ERC20 tokens, add approve call for the bridge amount
|
|
2782
|
+
// (CEA approves gateway to spend the Vault-deposited amount)
|
|
2783
|
+
if (tokenAddress !== selectors_1.ZERO_ADDRESS && bridgeAmount > BigInt(0)) {
|
|
2784
|
+
const approveData = (0, viem_1.encodeFunctionData)({
|
|
2785
|
+
abi: abi_1.ERC20_EVM,
|
|
2786
|
+
functionName: 'approve',
|
|
2787
|
+
args: [gatewayAddress, bridgeAmount],
|
|
2788
|
+
});
|
|
2789
|
+
ceaMulticalls.push({
|
|
2790
|
+
to: tokenAddress,
|
|
2791
|
+
value: BigInt(0),
|
|
2792
|
+
data: approveData,
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
// Pre-fetch UEA nonce — needed for the inbound UniversalPayload struct
|
|
2796
|
+
const ueaCode = yield this.pushClient.publicClient.getCode({ address: ueaAddress });
|
|
2797
|
+
const isUEADeployed = ueaCode !== undefined;
|
|
2798
|
+
const ueaNonce = isUEADeployed ? yield this.getUEANonce(ueaAddress) : BigInt(0);
|
|
2799
|
+
// Build payload for Push Chain execution (if any)
|
|
2800
|
+
// This is what happens AFTER funds arrive on Push Chain.
|
|
2801
|
+
// The relay expects a full UniversalPayload struct (to, value, data, gasLimit, ...),
|
|
2802
|
+
// where `data` contains the multicall payload (with UEA_MULTICALL_SELECTOR prefix).
|
|
2803
|
+
let pushPayload = '0x';
|
|
2804
|
+
if (params.data) {
|
|
2805
|
+
const multicallData = (0, payload_builders_1.buildExecuteMulticall)({
|
|
2806
|
+
execute: {
|
|
2807
|
+
to: pushDestination,
|
|
2808
|
+
value: params.value,
|
|
2809
|
+
data: params.data,
|
|
2810
|
+
},
|
|
2811
|
+
ueaAddress,
|
|
2812
|
+
});
|
|
2813
|
+
const multicallPayload = this._buildMulticallPayloadData(pushDestination, multicallData);
|
|
2814
|
+
// Use ueaNonce + 1: the outbound tx itself consumes one nonce via execute(),
|
|
2815
|
+
// so the inbound will arrive when the UEA nonce is already incremented.
|
|
2816
|
+
pushPayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonce + BigInt(1) });
|
|
2817
|
+
}
|
|
2818
|
+
// Build sendUniversalTxToUEA self-call on CEA
|
|
2819
|
+
// CEA multicall: to=CEA (self-call), value=0
|
|
2820
|
+
// CEA internally calls gateway.sendUniversalTxFromCEA(...)
|
|
2821
|
+
// Uses bridgeAmount (= burn amount deposited by Vault)
|
|
2822
|
+
const sendUniversalTxCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, // to: CEA address (self-call)
|
|
2823
|
+
tokenAddress, // token: address(0) for native, ERC20 address otherwise
|
|
2824
|
+
bridgeAmount, // amount: burn amount only (Vault-deposited)
|
|
2825
|
+
pushPayload, // payload: Push Chain execution payload
|
|
2826
|
+
ueaAddress // revertRecipient: UEA on Push Chain (receives refund if inbound fails)
|
|
2827
|
+
);
|
|
2828
|
+
ceaMulticalls.push(sendUniversalTxCall);
|
|
2829
|
+
// 7. Encode CEA multicalls into outbound payload
|
|
2830
|
+
// CEA will self-execute this multicall (to=CEA, value=0)
|
|
2831
|
+
const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
|
|
2832
|
+
this.printLog(`executeCeaToPush — CEA payload (first 100 chars): ${ceaPayload.slice(0, 100)}...`);
|
|
2833
|
+
// 8. Determine PRC-20 token for the outbound burn on Push Chain.
|
|
2834
|
+
// For ERC20 flows (params.funds with token), use the token's PRC-20 (e.g. pUSDT)
|
|
2835
|
+
// so the Vault deposits ERC20 to CEA. For native flows, use the chain's native PRC-20 (e.g. pBNB).
|
|
2836
|
+
let prc20Token;
|
|
2837
|
+
if (((_d = params.funds) === null || _d === void 0 ? void 0 : _d.amount) && params.funds.token) {
|
|
2838
|
+
const token = params.funds.token;
|
|
2839
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
2840
|
+
}
|
|
2841
|
+
else {
|
|
2842
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
2843
|
+
}
|
|
2844
|
+
// burnAmount = PRC20 to burn on Push Chain (NOT the bridge amount).
|
|
2845
|
+
// Vault deposits burnAmount to CEA. CEA uses burnAmount + pre-existing balance for the bridge.
|
|
2846
|
+
const burnAmount = amount;
|
|
2847
|
+
this.printLog(`executeCeaToPush — prc20Token: ${prc20Token}, burnAmount: ${burnAmount.toString()}`);
|
|
2848
|
+
// 9. Build outbound request (same structure as Route 2)
|
|
2849
|
+
// target = CEA address (for self-execution), value = 0 in payload
|
|
2850
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, // target: CEA address (to=CEA for self-execution)
|
|
2851
|
+
prc20Token, // token: native PRC-20 for source chain (for namespace lookup)
|
|
2852
|
+
burnAmount, // amount: 1 wei (precompile workaround)
|
|
2853
|
+
(_e = params.gasLimit) !== null && _e !== void 0 ? _e : BigInt(0), ceaPayload, // payload: ABI-encoded CEA multicall
|
|
2854
|
+
ueaAddress // revertRecipient: UEA
|
|
2855
|
+
);
|
|
2856
|
+
this.printLog(`executeCeaToPush — outbound request: ${JSON.stringify({
|
|
2857
|
+
target: outboundReq.target,
|
|
2858
|
+
token: outboundReq.token,
|
|
2859
|
+
amount: outboundReq.amount.toString(),
|
|
2860
|
+
gasLimit: outboundReq.gasLimit.toString(),
|
|
2861
|
+
payloadLength: outboundReq.payload.length,
|
|
2862
|
+
revertRecipient: outboundReq.revertRecipient,
|
|
2863
|
+
}, null, 2)}`);
|
|
2864
|
+
// 10. Fetch UEA balance — needed for gas value calculation
|
|
2865
|
+
// (UEA code + nonce already fetched above for the inbound UniversalPayload)
|
|
2866
|
+
const gatewayPcAddress = this.getUniversalGatewayPCAddress();
|
|
2867
|
+
const ueaBalance = yield this.pushClient.getBalance(ueaAddress);
|
|
2868
|
+
// 11. Query gas fees from UniversalCore
|
|
2869
|
+
let gasFee = BigInt(0);
|
|
2870
|
+
let nativeValueForGas = BigInt(0);
|
|
2871
|
+
let gasToken = selectors_1.ZERO_ADDRESS;
|
|
2872
|
+
if (prc20Token !== selectors_1.ZERO_ADDRESS) {
|
|
2873
|
+
const gasLimit = outboundReq.gasLimit;
|
|
2874
|
+
try {
|
|
2875
|
+
const result = yield this.queryOutboundGasFee(prc20Token, gasLimit);
|
|
2876
|
+
gasToken = result.gasToken;
|
|
2877
|
+
gasFee = result.gasFee;
|
|
2878
|
+
nativeValueForGas = result.nativeValueForGas;
|
|
2879
|
+
this.printLog(`executeCeaToPush — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}`);
|
|
2880
|
+
}
|
|
2881
|
+
catch (err) {
|
|
2882
|
+
throw new Error(`Failed to query outbound gas fee for Route 3: ${err}`);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
// Adjust nativeValueForGas using UEA balance (contract refunds excess)
|
|
2886
|
+
// Re-fetch balance to minimize staleness from gas fee query RPC roundtrips
|
|
2887
|
+
const currentBalance = yield this.pushClient.getBalance(ueaAddress);
|
|
2888
|
+
// Cosmos-EVM tx overhead costs ~1 PC per operation; 3 PC covers approve(s) + buffer.
|
|
2889
|
+
const OUTBOUND_GAS_RESERVE_R3 = BigInt(3e18);
|
|
2890
|
+
if (currentBalance > OUTBOUND_GAS_RESERVE_R3 && currentBalance - OUTBOUND_GAS_RESERVE_R3 > nativeValueForGas) {
|
|
2891
|
+
const adjustedValue = currentBalance - OUTBOUND_GAS_RESERVE_R3;
|
|
2892
|
+
this.printLog(`executeCeaToPush — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} (UEA balance: ${currentBalance.toString()})`);
|
|
2893
|
+
nativeValueForGas = adjustedValue;
|
|
2894
|
+
}
|
|
2895
|
+
// 12. Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
|
|
2896
|
+
const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
|
|
2897
|
+
prc20Token,
|
|
2898
|
+
gasToken,
|
|
2899
|
+
burnAmount,
|
|
2900
|
+
gasFee,
|
|
2901
|
+
nativeValueForGas,
|
|
2902
|
+
gatewayPcAddress,
|
|
2903
|
+
outboundRequest: outboundReq,
|
|
2904
|
+
});
|
|
2905
|
+
this.printLog(`executeCeaToPush — Push Chain multicall has ${pushChainMulticalls.length} operations`);
|
|
2906
|
+
// Sum native values from multicall entries for proper fee calculation
|
|
2907
|
+
const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => { var _a; return sum + ((_a = mc.value) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
|
|
2908
|
+
// 13. Execute through the normal execute() flow
|
|
2909
|
+
const executeParams = {
|
|
2910
|
+
to: ueaAddress,
|
|
2911
|
+
value: multicallNativeValue, // ensures correct requiredFunds calculation
|
|
2912
|
+
data: pushChainMulticalls,
|
|
2913
|
+
_ueaStatus: {
|
|
2914
|
+
isDeployed: isUEADeployed,
|
|
2915
|
+
nonce: ueaNonce,
|
|
2916
|
+
balance: ueaBalance,
|
|
2917
|
+
},
|
|
2918
|
+
_skipFeeLocking: true, // outbound executes on Push Chain, no external fee locking
|
|
2919
|
+
};
|
|
2920
|
+
const response = yield this.execute(executeParams);
|
|
2921
|
+
// Add Route 3 context to response
|
|
2922
|
+
response.chain = sourceChain;
|
|
2923
|
+
const chainInfo = chain_1.CHAIN_INFO[sourceChain];
|
|
2924
|
+
response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
|
|
2925
|
+
return response;
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Route 3 SVM: Execute CEA-to-Push for Solana chains.
|
|
2930
|
+
*
|
|
2931
|
+
* Unlike EVM Route 3 which builds CEA multicalls, SVM Route 3 encodes a
|
|
2932
|
+
* `send_universal_tx_to_uea` instruction as an execute payload targeting
|
|
2933
|
+
* the SVM gateway program (self-call). The drain amount is embedded in
|
|
2934
|
+
* the instruction data, not in the outbound request amount.
|
|
2935
|
+
*/
|
|
2936
|
+
executeCeaToPushSvm(params, sourceChain) {
|
|
2937
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
2938
|
+
var _a, _b;
|
|
2939
|
+
if (typeof params.to !== 'string') {
|
|
2940
|
+
throw new Error('Route 3 SVM (CEA → Push): to must be a Push Chain address (string), not a ChainTarget');
|
|
2941
|
+
}
|
|
2942
|
+
const ueaAddress = this.computeUEAOffchain();
|
|
2943
|
+
// Get gateway program ID from chain config and convert to 0x-hex 32 bytes
|
|
2944
|
+
const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
|
|
2945
|
+
if (!lockerContract) {
|
|
2946
|
+
throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
|
|
2947
|
+
}
|
|
2948
|
+
const programPk = new web3_js_1.PublicKey(lockerContract);
|
|
2949
|
+
const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
|
|
2950
|
+
this.printLog(`executeCeaToPushSvm — sourceChain: ${sourceChain}, gateway: ${lockerContract}`);
|
|
2951
|
+
// Determine drain amount and token
|
|
2952
|
+
let drainAmount = BigInt(0);
|
|
2953
|
+
let tokenMintHex;
|
|
2954
|
+
let prc20Token;
|
|
2955
|
+
if (((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) && params.funds.amount > BigInt(0)) {
|
|
2956
|
+
// SPL token drain
|
|
2957
|
+
drainAmount = params.funds.amount;
|
|
2958
|
+
const token = params.funds.token;
|
|
2959
|
+
if (token && token.address) {
|
|
2960
|
+
// Convert SPL mint address to 32-byte hex
|
|
2961
|
+
const mintPk = new web3_js_1.PublicKey(token.address);
|
|
2962
|
+
tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
|
|
2963
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
2964
|
+
}
|
|
2965
|
+
else {
|
|
2966
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
2970
|
+
// Native SOL drain
|
|
2971
|
+
drainAmount = params.value;
|
|
2972
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
// Payload-only Route 3 SVM: no funds to drain, just execute data on Push Chain
|
|
2976
|
+
drainAmount = BigInt(0);
|
|
2977
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
2978
|
+
}
|
|
2979
|
+
// Build the SVM CPI payload (send_universal_tx_to_uea wrapped in execute)
|
|
2980
|
+
// If params.data is provided, pass it as extraPayload for Push Chain execution
|
|
2981
|
+
let extraPayload;
|
|
2982
|
+
if (params.data && typeof params.data === 'string') {
|
|
2983
|
+
extraPayload = (0, viem_1.hexToBytes)(params.data);
|
|
2984
|
+
}
|
|
2985
|
+
// Derive CEA PDA as revert recipient: ["push_identity", ueaAddress_20_bytes]
|
|
2986
|
+
const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex'); // 20 bytes
|
|
2987
|
+
const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
|
|
2988
|
+
const ceaPdaHex = ('0x' + Buffer.from(ceaPda.toBytes()).toString('hex'));
|
|
2989
|
+
const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
|
|
2990
|
+
gatewayProgramHex,
|
|
2991
|
+
drainAmount,
|
|
2992
|
+
tokenMintHex,
|
|
2993
|
+
extraPayload,
|
|
2994
|
+
revertRecipientHex: ceaPdaHex,
|
|
2995
|
+
});
|
|
2996
|
+
this.printLog(`executeCeaToPushSvm — drainAmount: ${drainAmount.toString()}, payload length: ${(svmPayload.length - 2) / 2} bytes`);
|
|
2997
|
+
// burnAmount = 1 (minimum for precompile; drain amount lives inside the ixData)
|
|
2998
|
+
// The precompile rejects amount=0, so we use BigInt(1) as a workaround.
|
|
2999
|
+
const burnAmount = BigInt(1);
|
|
3000
|
+
// Build outbound request: target = gateway program (self-call)
|
|
3001
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_b = params.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0), svmPayload, ueaAddress);
|
|
3002
|
+
this.printLog(`executeCeaToPushSvm — outbound request: target=${outboundReq.target.slice(0, 20)}..., token=${outboundReq.token}`);
|
|
3003
|
+
// Pre-fetch UEA status early — balance is needed for gas value calculation
|
|
3004
|
+
const gatewayPcAddress = this.getUniversalGatewayPCAddress();
|
|
3005
|
+
const [ueaCode, ueaBalance] = yield Promise.all([
|
|
3006
|
+
this.pushClient.publicClient.getCode({ address: ueaAddress }),
|
|
3007
|
+
this.pushClient.getBalance(ueaAddress),
|
|
3008
|
+
]);
|
|
3009
|
+
const isUEADeployed = ueaCode !== undefined;
|
|
3010
|
+
const ueaNonce = isUEADeployed ? yield this.getUEANonce(ueaAddress) : BigInt(0);
|
|
3011
|
+
// Query gas fees
|
|
3012
|
+
let gasFee = BigInt(0);
|
|
3013
|
+
let nativeValueForGas = BigInt(0);
|
|
3014
|
+
let gasToken = selectors_1.ZERO_ADDRESS;
|
|
3015
|
+
if (prc20Token !== selectors_1.ZERO_ADDRESS) {
|
|
3016
|
+
try {
|
|
3017
|
+
const result = yield this.queryOutboundGasFee(prc20Token, outboundReq.gasLimit);
|
|
3018
|
+
gasToken = result.gasToken;
|
|
3019
|
+
gasFee = result.gasFee;
|
|
3020
|
+
nativeValueForGas = result.nativeValueForGas;
|
|
3021
|
+
this.printLog(`executeCeaToPushSvm — gasFee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}`);
|
|
3022
|
+
}
|
|
3023
|
+
catch (err) {
|
|
3024
|
+
throw new Error(`Failed to query outbound gas fee for SVM Route 3: ${err}`);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
// Adjust nativeValueForGas using UEA balance (contract refunds excess)
|
|
3028
|
+
// Re-fetch balance to minimize staleness from gas fee query RPC roundtrips
|
|
3029
|
+
const currentBalance = yield this.pushClient.getBalance(ueaAddress);
|
|
3030
|
+
// Cosmos-EVM tx overhead costs ~1 PC per operation; 3 PC covers approve(s) + buffer.
|
|
3031
|
+
const OUTBOUND_GAS_RESERVE_R3_SVM = BigInt(3e18);
|
|
3032
|
+
if (currentBalance > OUTBOUND_GAS_RESERVE_R3_SVM && currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM > nativeValueForGas) {
|
|
3033
|
+
const adjustedValue = currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM;
|
|
3034
|
+
this.printLog(`executeCeaToPushSvm — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} (UEA balance: ${currentBalance.toString()})`);
|
|
3035
|
+
nativeValueForGas = adjustedValue;
|
|
3036
|
+
}
|
|
3037
|
+
// Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
|
|
3038
|
+
const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
|
|
3039
|
+
prc20Token,
|
|
3040
|
+
gasToken,
|
|
3041
|
+
burnAmount,
|
|
3042
|
+
gasFee,
|
|
3043
|
+
nativeValueForGas,
|
|
3044
|
+
gatewayPcAddress,
|
|
3045
|
+
outboundRequest: outboundReq,
|
|
3046
|
+
});
|
|
3047
|
+
// Sum native values from multicall entries for proper fee calculation
|
|
3048
|
+
const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => { var _a; return sum + ((_a = mc.value) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
|
|
3049
|
+
// Execute through the normal execute() flow
|
|
3050
|
+
const executeParams = {
|
|
3051
|
+
to: ueaAddress,
|
|
3052
|
+
value: multicallNativeValue, // ensures correct requiredFunds calculation
|
|
3053
|
+
data: pushChainMulticalls,
|
|
3054
|
+
_ueaStatus: {
|
|
3055
|
+
isDeployed: isUEADeployed,
|
|
3056
|
+
nonce: ueaNonce,
|
|
3057
|
+
balance: ueaBalance,
|
|
3058
|
+
},
|
|
3059
|
+
_skipFeeLocking: true, // outbound executes on Push Chain, no external fee locking
|
|
3060
|
+
};
|
|
3061
|
+
const response = yield this.execute(executeParams);
|
|
3062
|
+
// Add Route 3 SVM context to response
|
|
3063
|
+
response.chain = sourceChain;
|
|
3064
|
+
const chainInfo = chain_1.CHAIN_INFO[sourceChain];
|
|
3065
|
+
response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
|
|
3066
|
+
return response;
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Route 4: Execute CEA to CEA transaction via Push Chain
|
|
3071
|
+
*
|
|
3072
|
+
* @param params - Universal execution parameters with from.chain and to.chain
|
|
3073
|
+
* @returns UniversalTxResponse
|
|
3074
|
+
*/
|
|
3075
|
+
executeCeaToCea(params) {
|
|
3076
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
3077
|
+
// CEA → CEA requires chaining Route 3 (CEA → Push) and Route 2 (Push → CEA)
|
|
3078
|
+
// This is a complex flow that requires coordination
|
|
3079
|
+
throw new Error('CEA → CEA transactions are not yet fully implemented. ' +
|
|
3080
|
+
'Use prepareTransaction().thenOn() to chain Route 3 → Route 2 manually.');
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Build payload for a specific route
|
|
3085
|
+
*/
|
|
3086
|
+
buildPayloadForRoute(params, route, nonce) {
|
|
3087
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
3088
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
3089
|
+
const ueaAddress = this.computeUEAOffchain();
|
|
3090
|
+
switch (route) {
|
|
3091
|
+
case route_detector_1.TransactionRoute.UOA_TO_PUSH: {
|
|
3092
|
+
// Build standard Push Chain payload
|
|
3093
|
+
const executeParams = this.toExecuteParams(params);
|
|
3094
|
+
const multicallData = (0, payload_builders_1.buildExecuteMulticall)({
|
|
3095
|
+
execute: executeParams,
|
|
3096
|
+
ueaAddress,
|
|
3097
|
+
});
|
|
3098
|
+
const payload = this._buildMulticallPayloadData(executeParams.to, multicallData);
|
|
3099
|
+
const req = this._buildUniversalTxRequest({
|
|
3100
|
+
recipient: viem_1.zeroAddress,
|
|
3101
|
+
token: viem_1.zeroAddress,
|
|
3102
|
+
amount: BigInt(0),
|
|
3103
|
+
payload,
|
|
3104
|
+
});
|
|
3105
|
+
return { payload, gatewayRequest: req };
|
|
3106
|
+
}
|
|
3107
|
+
case route_detector_1.TransactionRoute.UOA_TO_CEA: {
|
|
3108
|
+
const target = params.to;
|
|
3109
|
+
// Branch: SVM vs EVM
|
|
3110
|
+
if ((0, payload_builders_1.isSvmChain)(target.chain)) {
|
|
3111
|
+
// SVM: build SVM payload (binary or empty for withdraw)
|
|
3112
|
+
let payload = '0x';
|
|
3113
|
+
if (params.svmExecute) {
|
|
3114
|
+
payload = (0, payload_builders_1.encodeSvmExecutePayload)({
|
|
3115
|
+
targetProgram: params.svmExecute.targetProgram,
|
|
3116
|
+
accounts: params.svmExecute.accounts,
|
|
3117
|
+
ixData: params.svmExecute.ixData,
|
|
3118
|
+
instructionId: 2,
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
const targetBytes = (_b = (_a = params.svmExecute) === null || _a === void 0 ? void 0 : _a.targetProgram) !== null && _b !== void 0 ? _b : target.address;
|
|
3122
|
+
let prc20Token = selectors_1.ZERO_ADDRESS;
|
|
3123
|
+
let burnAmount = BigInt(0);
|
|
3124
|
+
if ((_c = params.funds) === null || _c === void 0 ? void 0 : _c.amount) {
|
|
3125
|
+
const token = params.funds.token;
|
|
3126
|
+
if (token) {
|
|
3127
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
3128
|
+
burnAmount = params.funds.amount;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
3132
|
+
prc20Token = this.getNativePRC20ForChain(target.chain);
|
|
3133
|
+
burnAmount = params.value;
|
|
3134
|
+
}
|
|
3135
|
+
else if (params.svmExecute) {
|
|
3136
|
+
prc20Token = this.getNativePRC20ForChain(target.chain);
|
|
3137
|
+
burnAmount = BigInt(1);
|
|
3138
|
+
}
|
|
3139
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_d = params.gasLimit) !== null && _d !== void 0 ? _d : BigInt(0), payload, ueaAddress);
|
|
3140
|
+
return { payload, gatewayRequest: outboundReq };
|
|
3141
|
+
}
|
|
3142
|
+
// EVM path: Resolve CEA address first (needed for self-transfer check)
|
|
3143
|
+
const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, target.chain, (_e = this.rpcUrls[target.chain]) === null || _e === void 0 ? void 0 : _e[0]);
|
|
3144
|
+
// Build CEA outbound payload
|
|
3145
|
+
const multicalls = [];
|
|
3146
|
+
if (params.data) {
|
|
3147
|
+
if (Array.isArray(params.data)) {
|
|
3148
|
+
multicalls.push(...params.data);
|
|
3149
|
+
}
|
|
3150
|
+
else {
|
|
3151
|
+
// Single call with data. Native value (if any) is already delivered to
|
|
3152
|
+
// CEA by the Vault via executeUniversalTx{value: amount}(). Attaching
|
|
3153
|
+
// value to the call would revert if the target function is not payable.
|
|
3154
|
+
// To call a payable function with value, use explicit multicalls.
|
|
3155
|
+
multicalls.push({
|
|
3156
|
+
to: target.address,
|
|
3157
|
+
value: BigInt(0),
|
|
3158
|
+
data: params.data,
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
else if (params.value) {
|
|
3163
|
+
// Skip multicall when sending native value to own CEA — gateway deposits directly.
|
|
3164
|
+
// Self-call with value would revert (CEA._handleMulticall rejects it).
|
|
3165
|
+
if (target.address.toLowerCase() !== ceaAddress.toLowerCase()) {
|
|
3166
|
+
multicalls.push({
|
|
3167
|
+
to: target.address,
|
|
3168
|
+
value: params.value,
|
|
3169
|
+
data: '0x',
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
const payload = (0, payload_builders_1.buildCeaMulticallPayload)(multicalls);
|
|
3174
|
+
let prc20Token = selectors_1.ZERO_ADDRESS;
|
|
3175
|
+
let burnAmount = BigInt(0);
|
|
3176
|
+
if ((_f = params.funds) === null || _f === void 0 ? void 0 : _f.amount) {
|
|
3177
|
+
const token = params.funds.token;
|
|
3178
|
+
if (token) {
|
|
3179
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
3180
|
+
burnAmount = params.funds.amount;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
const targetBytes = ceaAddress;
|
|
3184
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_g = params.gasLimit) !== null && _g !== void 0 ? _g : BigInt(0), payload, ueaAddress);
|
|
3185
|
+
return { payload, gatewayRequest: outboundReq };
|
|
3186
|
+
}
|
|
3187
|
+
case route_detector_1.TransactionRoute.CEA_TO_PUSH: {
|
|
3188
|
+
// Route 3: CEA → Push Chain
|
|
3189
|
+
// Build CEA multicall (approve + sendUniversalTxFromCEA) and wrap in outbound
|
|
3190
|
+
if (!((_h = params.from) === null || _h === void 0 ? void 0 : _h.chain)) {
|
|
3191
|
+
throw new Error('Route 3 (CEA → Push) requires from.chain');
|
|
3192
|
+
}
|
|
3193
|
+
const sourceChain = params.from.chain;
|
|
3194
|
+
const pushDestination = params.to;
|
|
3195
|
+
// SVM chains use gateway self-call, not CEA multicall
|
|
3196
|
+
if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
|
|
3197
|
+
const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
|
|
3198
|
+
if (!lockerContract) {
|
|
3199
|
+
throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
|
|
3200
|
+
}
|
|
3201
|
+
const programPk = new web3_js_1.PublicKey(lockerContract);
|
|
3202
|
+
const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
|
|
3203
|
+
let drainAmount = BigInt(0);
|
|
3204
|
+
let tokenMintHex;
|
|
3205
|
+
let prc20Token;
|
|
3206
|
+
if (((_j = params.funds) === null || _j === void 0 ? void 0 : _j.amount) && params.funds.amount > BigInt(0)) {
|
|
3207
|
+
drainAmount = params.funds.amount;
|
|
3208
|
+
const token = params.funds.token;
|
|
3209
|
+
if (token && token.address) {
|
|
3210
|
+
const mintPk = new web3_js_1.PublicKey(token.address);
|
|
3211
|
+
tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
|
|
3212
|
+
prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
|
|
3213
|
+
}
|
|
3214
|
+
else {
|
|
3215
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
3219
|
+
drainAmount = params.value;
|
|
3220
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
3221
|
+
}
|
|
3222
|
+
else {
|
|
3223
|
+
// Payload-only Route 3 SVM: no funds to drain, just execute data on Push Chain
|
|
3224
|
+
drainAmount = BigInt(0);
|
|
3225
|
+
prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
3226
|
+
}
|
|
3227
|
+
// Derive CEA PDA as revert recipient
|
|
3228
|
+
const ueaBytes2 = Buffer.from(ueaAddress.slice(2), 'hex');
|
|
3229
|
+
const [ceaPda2] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes2], programPk);
|
|
3230
|
+
const ceaPdaHex2 = ('0x' + Buffer.from(ceaPda2.toBytes()).toString('hex'));
|
|
3231
|
+
const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
|
|
3232
|
+
gatewayProgramHex,
|
|
3233
|
+
drainAmount,
|
|
3234
|
+
tokenMintHex,
|
|
3235
|
+
revertRecipientHex: ceaPdaHex2,
|
|
3236
|
+
});
|
|
3237
|
+
// burnAmount = 1 (minimum for precompile; drain amount lives inside the ixData)
|
|
3238
|
+
const burnAmount = BigInt(1);
|
|
3239
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_k = params.gasLimit) !== null && _k !== void 0 ? _k : BigInt(0), svmPayload, ueaAddress);
|
|
3240
|
+
return { payload: svmPayload, gatewayRequest: outboundReq };
|
|
3241
|
+
}
|
|
3242
|
+
const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_l = this.rpcUrls[sourceChain]) === null || _l === void 0 ? void 0 : _l[0]);
|
|
3243
|
+
const gatewayAddr = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
|
|
3244
|
+
if (!gatewayAddr) {
|
|
3245
|
+
throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
|
|
3246
|
+
}
|
|
3247
|
+
// Build CEA multicalls (self-calls via sendUniversalTxToUEA)
|
|
3248
|
+
const ceaMulticalls = [];
|
|
3249
|
+
let tokenAddress = selectors_1.ZERO_ADDRESS;
|
|
3250
|
+
let amount = BigInt(0);
|
|
3251
|
+
if ((_m = params.funds) === null || _m === void 0 ? void 0 : _m.amount) {
|
|
3252
|
+
const token = params.funds.token;
|
|
3253
|
+
if (token) {
|
|
3254
|
+
if (token.mechanism === 'native') {
|
|
3255
|
+
tokenAddress = selectors_1.ZERO_ADDRESS;
|
|
3256
|
+
amount = params.funds.amount;
|
|
3257
|
+
}
|
|
3258
|
+
else {
|
|
3259
|
+
tokenAddress = token.address;
|
|
3260
|
+
amount = params.funds.amount;
|
|
3261
|
+
// Approve gateway for ERC20 (CEA self-call, value=0)
|
|
3262
|
+
const approveData = (0, viem_1.encodeFunctionData)({
|
|
3263
|
+
abi: abi_1.ERC20_EVM,
|
|
3264
|
+
functionName: 'approve',
|
|
3265
|
+
args: [gatewayAddr, amount],
|
|
3266
|
+
});
|
|
3267
|
+
ceaMulticalls.push({
|
|
3268
|
+
to: tokenAddress,
|
|
3269
|
+
value: BigInt(0),
|
|
3270
|
+
data: approveData,
|
|
3271
|
+
});
|
|
972
3272
|
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
else if (params.value && params.value > BigInt(0)) {
|
|
3276
|
+
tokenAddress = selectors_1.ZERO_ADDRESS;
|
|
3277
|
+
amount = params.value;
|
|
3278
|
+
}
|
|
3279
|
+
// Fetch UEA nonce for inbound UniversalPayload
|
|
3280
|
+
const ueaCodeHop = yield this.pushClient.publicClient.getCode({ address: ueaAddress });
|
|
3281
|
+
const ueaNonceHop = ueaCodeHop !== undefined ? yield this.getUEANonce(ueaAddress) : BigInt(0);
|
|
3282
|
+
// Build Push Chain payload (what executes after inbound arrives)
|
|
3283
|
+
// Wrap in UniversalPayload struct for the relay.
|
|
3284
|
+
let pushPayload = '0x';
|
|
3285
|
+
if (params.data) {
|
|
3286
|
+
const multicallData = (0, payload_builders_1.buildExecuteMulticall)({
|
|
3287
|
+
execute: {
|
|
3288
|
+
to: pushDestination,
|
|
3289
|
+
value: params.value,
|
|
3290
|
+
data: params.data,
|
|
3291
|
+
},
|
|
3292
|
+
ueaAddress,
|
|
3293
|
+
});
|
|
3294
|
+
const multicallPayload = this._buildMulticallPayloadData(pushDestination, multicallData);
|
|
3295
|
+
pushPayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonceHop + BigInt(1) });
|
|
3296
|
+
}
|
|
3297
|
+
// Build sendUniversalTxToUEA self-call on CEA (to=CEA, value=0)
|
|
3298
|
+
const sendCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, pushPayload, ceaAddress);
|
|
3299
|
+
ceaMulticalls.push(sendCall);
|
|
3300
|
+
const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
|
|
3301
|
+
const prc20Token = this.getNativePRC20ForChain(sourceChain);
|
|
3302
|
+
// burnAmount = actual amount needed by CEA. Vault deposits this as msg.value.
|
|
3303
|
+
// Fallback to BigInt(1) for payload-only outbound (precompile rejects amount=0).
|
|
3304
|
+
const burnAmount = amount > BigInt(0) ? amount : BigInt(1);
|
|
3305
|
+
const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, (_o = params.gasLimit) !== null && _o !== void 0 ? _o : BigInt(0), ceaPayload, ueaAddress);
|
|
3306
|
+
return { payload: ceaPayload, gatewayRequest: outboundReq };
|
|
3307
|
+
}
|
|
3308
|
+
default:
|
|
3309
|
+
throw new Error(`Cannot build payload for route: ${route}`);
|
|
980
3310
|
}
|
|
981
3311
|
});
|
|
982
3312
|
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Convert UniversalExecuteParams to ExecuteParams for backwards compatibility
|
|
3315
|
+
*/
|
|
3316
|
+
toExecuteParams(params) {
|
|
3317
|
+
// Extract address from ChainTarget if needed
|
|
3318
|
+
const to = typeof params.to === 'string'
|
|
3319
|
+
? params.to
|
|
3320
|
+
: params.to.address;
|
|
3321
|
+
return {
|
|
3322
|
+
to,
|
|
3323
|
+
value: params.value,
|
|
3324
|
+
data: params.data,
|
|
3325
|
+
funds: params.funds,
|
|
3326
|
+
gasLimit: params.gasLimit,
|
|
3327
|
+
maxFeePerGas: params.maxFeePerGas,
|
|
3328
|
+
maxPriorityFeePerGas: params.maxPriorityFeePerGas,
|
|
3329
|
+
deadline: params.deadline,
|
|
3330
|
+
payGasWith: params.payGasWith,
|
|
3331
|
+
feeLockTxHash: params.feeLockTxHash,
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Get the UniversalGatewayPC address for the current Push network
|
|
3336
|
+
*/
|
|
3337
|
+
getUniversalGatewayPCAddress() {
|
|
3338
|
+
// UniversalGatewayPC is a precompile at a fixed address on Push Chain
|
|
3339
|
+
// Address: 0x00000000000000000000000000000000000000C1
|
|
3340
|
+
return '0x00000000000000000000000000000000000000C1';
|
|
3341
|
+
}
|
|
3342
|
+
/**
|
|
3343
|
+
* Get the native PRC-20 token address on Push Chain for a target external chain.
|
|
3344
|
+
* Maps chains to their native asset representations on Push Chain.
|
|
3345
|
+
*
|
|
3346
|
+
* @param targetChain - The target external chain
|
|
3347
|
+
* @returns PRC-20 token address on Push Chain
|
|
3348
|
+
*/
|
|
3349
|
+
getNativePRC20ForChain(targetChain) {
|
|
3350
|
+
const synthetics = chain_1.SYNTHETIC_PUSH_ERC20[this.pushNetwork];
|
|
3351
|
+
switch (targetChain) {
|
|
3352
|
+
case enums_1.CHAIN.ETHEREUM_SEPOLIA:
|
|
3353
|
+
case enums_1.CHAIN.ETHEREUM_MAINNET:
|
|
3354
|
+
return synthetics.pETH;
|
|
3355
|
+
case enums_1.CHAIN.ARBITRUM_SEPOLIA:
|
|
3356
|
+
return synthetics.pETH_ARB;
|
|
3357
|
+
case enums_1.CHAIN.BASE_SEPOLIA:
|
|
3358
|
+
return synthetics.pETH_BASE;
|
|
3359
|
+
case enums_1.CHAIN.BNB_TESTNET:
|
|
3360
|
+
return synthetics.pETH_BNB;
|
|
3361
|
+
case enums_1.CHAIN.SOLANA_DEVNET:
|
|
3362
|
+
case enums_1.CHAIN.SOLANA_TESTNET:
|
|
3363
|
+
case enums_1.CHAIN.SOLANA_MAINNET:
|
|
3364
|
+
return synthetics.pSOL;
|
|
3365
|
+
default:
|
|
3366
|
+
throw new Error(`No native PRC-20 token mapping for chain ${targetChain}. ` +
|
|
3367
|
+
`Use 'funds' parameter to specify the token explicitly.`);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* Get CAIP-2 chain namespace for a chain
|
|
3372
|
+
*/
|
|
3373
|
+
getChainNamespace(chain) {
|
|
3374
|
+
const { vm, chainId } = chain_1.CHAIN_INFO[chain];
|
|
3375
|
+
const namespace = chain_1.VM_NAMESPACE[vm];
|
|
3376
|
+
return `${namespace}:${chainId}`;
|
|
3377
|
+
}
|
|
983
3378
|
/**
|
|
984
3379
|
* Locks a fee on the origin chain by interacting with the chain's fee-locker contract.
|
|
985
3380
|
*
|
|
@@ -1015,22 +3410,7 @@ class Orchestrator {
|
|
|
1015
3410
|
(nativeTokenUsdPrice - BigInt(1))) /
|
|
1016
3411
|
nativeTokenUsdPrice;
|
|
1017
3412
|
nativeAmount = nativeAmount + BigInt(1);
|
|
1018
|
-
|
|
1019
|
-
// recipient: zeroAddress,
|
|
1020
|
-
// token: zeroAddress,
|
|
1021
|
-
// amount: BigInt(0),
|
|
1022
|
-
// payload: payloadBytes,
|
|
1023
|
-
// revertInstruction: revertCFG,
|
|
1024
|
-
// signatureData: '0x',
|
|
1025
|
-
// } as unknown as never;
|
|
1026
|
-
const txHash = yield evmClient.writeContract({
|
|
1027
|
-
abi: abi_1.UNIVERSAL_GATEWAY_V0,
|
|
1028
|
-
address: lockerContract,
|
|
1029
|
-
functionName: 'sendUniversalTx',
|
|
1030
|
-
args: [req],
|
|
1031
|
-
signer: this.universalSigner,
|
|
1032
|
-
value: nativeAmount,
|
|
1033
|
-
});
|
|
3413
|
+
const txHash = yield this.sendGatewayTxWithFallback(evmClient, lockerContract, req, this.universalSigner, nativeAmount);
|
|
1034
3414
|
return (0, viem_1.hexToBytes)(txHash);
|
|
1035
3415
|
}
|
|
1036
3416
|
case enums_1.VM.SVM: {
|
|
@@ -1058,21 +3438,19 @@ class Orchestrator {
|
|
|
1058
3438
|
const [rateLimitConfigPda] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('rate_limit_config')], programId);
|
|
1059
3439
|
const [tokenRateLimitPda] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('rate_limit'), web3_js_1.PublicKey.default.toBuffer()], programId);
|
|
1060
3440
|
const userPk = new web3_js_1.PublicKey(this.universalSigner.account.address);
|
|
1061
|
-
const
|
|
1062
|
-
fundRecipient: userPk,
|
|
1063
|
-
revertMsg: Buffer.from([]),
|
|
1064
|
-
};
|
|
3441
|
+
const { feeVaultPda, protocolFeeLamports } = yield this._getSvmProtocolFee(svmClient, programId);
|
|
1065
3442
|
const gasReq = this._buildSvmUniversalTxRequestFromReq(req, userPk);
|
|
1066
3443
|
try {
|
|
1067
3444
|
const txHash = yield svmClient.writeContract({
|
|
1068
3445
|
abi: abi_1.SVM_GATEWAY_IDL,
|
|
1069
3446
|
address: programId.toBase58(),
|
|
1070
3447
|
functionName: 'sendUniversalTx',
|
|
1071
|
-
args: [gasReq, nativeAmount],
|
|
3448
|
+
args: [gasReq, nativeAmount + protocolFeeLamports],
|
|
1072
3449
|
signer: this.universalSigner,
|
|
1073
3450
|
accounts: {
|
|
1074
3451
|
config: configPda,
|
|
1075
3452
|
vault: vaultPda,
|
|
3453
|
+
feeVault: feeVaultPda,
|
|
1076
3454
|
userTokenAccount: vaultPda,
|
|
1077
3455
|
gatewayTokenAccount: vaultPda,
|
|
1078
3456
|
user: userPk,
|
|
@@ -1109,12 +3487,200 @@ class Orchestrator {
|
|
|
1109
3487
|
signatureData: '0x',
|
|
1110
3488
|
};
|
|
1111
3489
|
}
|
|
3490
|
+
/**
|
|
3491
|
+
* Returns the resolved gateway version for the current origin chain.
|
|
3492
|
+
* Checks the per-chain runtime cache first, then falls back to static config.
|
|
3493
|
+
* TODO: Remove V0 fallback once all chains are upgraded to V1.
|
|
3494
|
+
*/
|
|
3495
|
+
getGatewayVersion() {
|
|
3496
|
+
var _a;
|
|
3497
|
+
const chain = this.universalSigner.account.chain;
|
|
3498
|
+
// 1. Check runtime cache (set after a successful tx)
|
|
3499
|
+
const cached = this.gatewayVersionCache.get(chain);
|
|
3500
|
+
if (cached)
|
|
3501
|
+
return cached;
|
|
3502
|
+
// 2. Fall back to static config
|
|
3503
|
+
return (_a = chain_1.CHAIN_INFO[chain].gatewayVersion) !== null && _a !== void 0 ? _a : 'v0';
|
|
3504
|
+
}
|
|
3505
|
+
/**
|
|
3506
|
+
* Returns true if the current origin chain uses the V1 gateway (revertRecipient address).
|
|
3507
|
+
*/
|
|
3508
|
+
isV1Gateway() {
|
|
3509
|
+
return this.getGatewayVersion() === 'v1';
|
|
3510
|
+
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Returns the correct gateway ABI for the given version.
|
|
3513
|
+
*/
|
|
3514
|
+
getGatewayAbiForVersion(version) {
|
|
3515
|
+
return version === 'v1'
|
|
3516
|
+
? abi_1.UNIVERSAL_GATEWAY_V1_SEND
|
|
3517
|
+
: abi_1.UNIVERSAL_GATEWAY_V0;
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Returns the correct gateway ABI based on the origin chain's gateway version.
|
|
3521
|
+
*/
|
|
3522
|
+
getGatewayAbi() {
|
|
3523
|
+
return this.getGatewayAbiForVersion(this.getGatewayVersion());
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Converts a V0 UniversalTxRequest to V1 format.
|
|
3527
|
+
*/
|
|
3528
|
+
toGatewayRequestV1(req) {
|
|
3529
|
+
return {
|
|
3530
|
+
recipient: req.recipient,
|
|
3531
|
+
token: req.token,
|
|
3532
|
+
amount: req.amount,
|
|
3533
|
+
payload: req.payload,
|
|
3534
|
+
revertRecipient: req.revertInstruction.fundRecipient,
|
|
3535
|
+
signatureData: req.signatureData,
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3539
|
+
* Converts a V0 UniversalTxRequest to the correct format based on gateway version.
|
|
3540
|
+
*/
|
|
3541
|
+
toGatewayRequest(req) {
|
|
3542
|
+
if (!this.isV1Gateway())
|
|
3543
|
+
return req;
|
|
3544
|
+
return this.toGatewayRequestV1(req);
|
|
3545
|
+
}
|
|
3546
|
+
/**
|
|
3547
|
+
* Converts a V0 UniversalTokenTxRequest to V1 format.
|
|
3548
|
+
*/
|
|
3549
|
+
toGatewayTokenRequestV1(req) {
|
|
3550
|
+
return {
|
|
3551
|
+
recipient: req.recipient,
|
|
3552
|
+
token: req.token,
|
|
3553
|
+
amount: req.amount,
|
|
3554
|
+
gasToken: req.gasToken,
|
|
3555
|
+
gasAmount: req.gasAmount,
|
|
3556
|
+
payload: req.payload,
|
|
3557
|
+
revertRecipient: req.revertInstruction.fundRecipient,
|
|
3558
|
+
signatureData: req.signatureData,
|
|
3559
|
+
amountOutMinETH: req.amountOutMinETH,
|
|
3560
|
+
deadline: req.deadline,
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
/**
|
|
3564
|
+
* Converts a V0 UniversalTokenTxRequest to the correct format based on gateway version.
|
|
3565
|
+
*/
|
|
3566
|
+
toGatewayTokenRequest(req) {
|
|
3567
|
+
if (!this.isV1Gateway())
|
|
3568
|
+
return req;
|
|
3569
|
+
return this.toGatewayTokenRequestV1(req);
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* Sends a gateway transaction with V1-first, V0-fallback strategy.
|
|
3573
|
+
*
|
|
3574
|
+
* Strategy:
|
|
3575
|
+
* 1. Try V1 (revertRecipient address format) first.
|
|
3576
|
+
* 2. If V1 fails (contract not yet upgraded), retry with V0 (revertInstruction struct).
|
|
3577
|
+
* 3. Cache the working version per chain so subsequent calls skip the fallback.
|
|
3578
|
+
*
|
|
3579
|
+
* TODO: Remove V0 fallback once all chains are upgraded to V1. After that,
|
|
3580
|
+
* delete this method and call writeContract directly with V1 ABI.
|
|
3581
|
+
*/
|
|
3582
|
+
sendGatewayTxWithFallback(evmClient, address, req, signer, value) {
|
|
3583
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
3584
|
+
const chain = this.universalSigner.account.chain;
|
|
3585
|
+
const currentVersion = this.getGatewayVersion();
|
|
3586
|
+
// If we already know the version (from cache or config), use it directly — no fallback needed.
|
|
3587
|
+
if (this.gatewayVersionCache.has(chain)) {
|
|
3588
|
+
return evmClient.writeContract({
|
|
3589
|
+
abi: this.getGatewayAbiForVersion(currentVersion),
|
|
3590
|
+
address,
|
|
3591
|
+
functionName: 'sendUniversalTx',
|
|
3592
|
+
args: [currentVersion === 'v1' ? this.toGatewayRequestV1(req) : req],
|
|
3593
|
+
signer,
|
|
3594
|
+
value,
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3597
|
+
// No cached version yet — try V1 first, fall back to V0.
|
|
3598
|
+
// TODO: Remove this try/catch block once all chains are on V1.
|
|
3599
|
+
try {
|
|
3600
|
+
this.printLog(`[Gateway] Trying V1 format for chain ${chain}...`);
|
|
3601
|
+
const txHash = yield evmClient.writeContract({
|
|
3602
|
+
abi: this.getGatewayAbiForVersion('v1'),
|
|
3603
|
+
address,
|
|
3604
|
+
functionName: 'sendUniversalTx',
|
|
3605
|
+
args: [this.toGatewayRequestV1(req)],
|
|
3606
|
+
signer,
|
|
3607
|
+
value,
|
|
3608
|
+
});
|
|
3609
|
+
// V1 succeeded — cache it
|
|
3610
|
+
this.gatewayVersionCache.set(chain, 'v1');
|
|
3611
|
+
this.printLog(`[Gateway] V1 succeeded for chain ${chain}, cached.`);
|
|
3612
|
+
return txHash;
|
|
3613
|
+
}
|
|
3614
|
+
catch (v1Error) {
|
|
3615
|
+
this.printLog(`[Gateway] V1 failed for chain ${chain}, falling back to V0... Error: ${v1Error}`);
|
|
3616
|
+
}
|
|
3617
|
+
// V0 fallback — contract not yet upgraded on this chain
|
|
3618
|
+
// TODO: Remove this block once all chains are upgraded to V1.
|
|
3619
|
+
try {
|
|
3620
|
+
const txHash = yield evmClient.writeContract({
|
|
3621
|
+
abi: this.getGatewayAbiForVersion('v0'),
|
|
3622
|
+
address,
|
|
3623
|
+
functionName: 'sendUniversalTx',
|
|
3624
|
+
args: [req],
|
|
3625
|
+
signer,
|
|
3626
|
+
value,
|
|
3627
|
+
});
|
|
3628
|
+
// V0 succeeded — cache it
|
|
3629
|
+
this.gatewayVersionCache.set(chain, 'v0');
|
|
3630
|
+
this.printLog(`[Gateway] V0 succeeded for chain ${chain}, cached.`);
|
|
3631
|
+
return txHash;
|
|
3632
|
+
}
|
|
3633
|
+
catch (v0Error) {
|
|
3634
|
+
// Both versions failed — throw the V0 error (more likely to be the real issue on non-upgraded chains)
|
|
3635
|
+
throw v0Error;
|
|
3636
|
+
}
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
/**
|
|
3640
|
+
* Sends a gateway token transaction with V1-first, V0-fallback strategy.
|
|
3641
|
+
* Same as sendGatewayTxWithFallback but for UniversalTokenTxRequest (gas token path).
|
|
3642
|
+
*
|
|
3643
|
+
* TODO: Remove V0 fallback once all chains are upgraded to V1.
|
|
3644
|
+
*/
|
|
3645
|
+
sendGatewayTokenTxWithFallback(evmClient, address, req, signer, value) {
|
|
3646
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
3647
|
+
const chain = this.universalSigner.account.chain;
|
|
3648
|
+
const currentVersion = this.getGatewayVersion();
|
|
3649
|
+
// If we already know the version (from cache or config), use it directly.
|
|
3650
|
+
if (this.gatewayVersionCache.has(chain)) {
|
|
3651
|
+
return evmClient.writeContract(Object.assign({ abi: this.getGatewayAbiForVersion(currentVersion), address, functionName: 'sendUniversalTx', args: [currentVersion === 'v1' ? this.toGatewayTokenRequestV1(req) : req], signer }, (value !== undefined && { value })));
|
|
3652
|
+
}
|
|
3653
|
+
// No cached version yet — try V1 first, fall back to V0.
|
|
3654
|
+
// TODO: Remove this try/catch block once all chains are on V1.
|
|
3655
|
+
try {
|
|
3656
|
+
this.printLog(`[Gateway] Trying V1 token format for chain ${chain}...`);
|
|
3657
|
+
const txHash = yield evmClient.writeContract(Object.assign({ abi: this.getGatewayAbiForVersion('v1'), address, functionName: 'sendUniversalTx', args: [this.toGatewayTokenRequestV1(req)], signer }, (value !== undefined && { value })));
|
|
3658
|
+
this.gatewayVersionCache.set(chain, 'v1');
|
|
3659
|
+
this.printLog(`[Gateway] V1 token tx succeeded for chain ${chain}, cached.`);
|
|
3660
|
+
return txHash;
|
|
3661
|
+
}
|
|
3662
|
+
catch (v1Error) {
|
|
3663
|
+
this.printLog(`[Gateway] V1 token tx failed for chain ${chain}, falling back to V0... Error: ${v1Error}`);
|
|
3664
|
+
}
|
|
3665
|
+
// V0 fallback
|
|
3666
|
+
// TODO: Remove this block once all chains are upgraded to V1.
|
|
3667
|
+
try {
|
|
3668
|
+
const txHash = yield evmClient.writeContract(Object.assign({ abi: this.getGatewayAbiForVersion('v0'), address, functionName: 'sendUniversalTx', args: [req], signer }, (value !== undefined && { value })));
|
|
3669
|
+
this.gatewayVersionCache.set(chain, 'v0');
|
|
3670
|
+
this.printLog(`[Gateway] V0 token tx succeeded for chain ${chain}, cached.`);
|
|
3671
|
+
return txHash;
|
|
3672
|
+
}
|
|
3673
|
+
catch (v0Error) {
|
|
3674
|
+
throw v0Error;
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
1112
3678
|
/**
|
|
1113
3679
|
* Builds the SVM UniversalTxRequest object from an existing EVM-style UniversalTxRequest.
|
|
1114
3680
|
* This allows reusing the same request shape for both EVM and SVM while only translating
|
|
1115
3681
|
* field encodings (addresses, bytes) to the Solana program format.
|
|
1116
3682
|
*/
|
|
1117
|
-
_buildSvmUniversalTxRequestFromReq(req,
|
|
3683
|
+
_buildSvmUniversalTxRequestFromReq(req, revertRecipient, signatureDataOverride) {
|
|
1118
3684
|
// recipient in EVM is a 20-byte address; the SVM gateway expects this as [u8; 20]
|
|
1119
3685
|
const recipientBytes = (0, viem_1.hexToBytes)(req.recipient);
|
|
1120
3686
|
const recipient = Array.from(recipientBytes.subarray(0, 20));
|
|
@@ -1143,7 +3709,7 @@ class Orchestrator {
|
|
|
1143
3709
|
token,
|
|
1144
3710
|
amount: req.amount,
|
|
1145
3711
|
payload: req.payload,
|
|
1146
|
-
|
|
3712
|
+
revertRecipient,
|
|
1147
3713
|
signatureData: signatureDataOverride !== null && signatureDataOverride !== void 0 ? signatureDataOverride : req.signatureData,
|
|
1148
3714
|
});
|
|
1149
3715
|
}
|
|
@@ -1151,7 +3717,7 @@ class Orchestrator {
|
|
|
1151
3717
|
* Builds the SVM UniversalTxRequest object expected by the Solana gateway program.
|
|
1152
3718
|
* Shape mirrors the request used in `svm-gateway` tests.
|
|
1153
3719
|
*/
|
|
1154
|
-
_buildSvmUniversalTxRequest({ recipient, token, amount, payload,
|
|
3720
|
+
_buildSvmUniversalTxRequest({ recipient, token, amount, payload, revertRecipient, signatureData, }) {
|
|
1155
3721
|
const payloadBuf = typeof payload === 'string' && payload.startsWith('0x')
|
|
1156
3722
|
? (() => {
|
|
1157
3723
|
const hex = payload.slice(2);
|
|
@@ -1188,13 +3754,29 @@ class Orchestrator {
|
|
|
1188
3754
|
token,
|
|
1189
3755
|
amount,
|
|
1190
3756
|
payload: payloadBuf,
|
|
1191
|
-
|
|
1192
|
-
fundRecipient,
|
|
1193
|
-
revertMsg: Buffer.alloc(0),
|
|
1194
|
-
},
|
|
3757
|
+
revertRecipient: revertRecipient,
|
|
1195
3758
|
signatureData: signatureBuf,
|
|
1196
3759
|
};
|
|
1197
3760
|
}
|
|
3761
|
+
_getSvmProtocolFee(svmClient, programId) {
|
|
3762
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
3763
|
+
var _a, _b, _c;
|
|
3764
|
+
const [feeVaultPda] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('fee_vault')], programId);
|
|
3765
|
+
try {
|
|
3766
|
+
const feeVault = yield svmClient.readContract({
|
|
3767
|
+
abi: abi_1.SVM_GATEWAY_IDL,
|
|
3768
|
+
address: abi_1.SVM_GATEWAY_IDL.address,
|
|
3769
|
+
functionName: 'feeVault',
|
|
3770
|
+
args: [feeVaultPda.toBase58()],
|
|
3771
|
+
});
|
|
3772
|
+
const protocolFeeLamports = BigInt((_c = (_b = ((_a = feeVault.protocolFeeLamports) !== null && _a !== void 0 ? _a : feeVault.protocol_fee_lamports)) === null || _b === void 0 ? void 0 : _b.toString()) !== null && _c !== void 0 ? _c : '0');
|
|
3773
|
+
return { feeVaultPda, protocolFeeLamports };
|
|
3774
|
+
}
|
|
3775
|
+
catch (_d) {
|
|
3776
|
+
return { feeVaultPda, protocolFeeLamports: BigInt(0) };
|
|
3777
|
+
}
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
1198
3780
|
signUniversalPayload(universalPayload, verifyingContract, version) {
|
|
1199
3781
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
1200
3782
|
const chain = this.universalSigner.account.chain;
|
|
@@ -1246,7 +3828,7 @@ class Orchestrator {
|
|
|
1246
3828
|
*/
|
|
1247
3829
|
sendUniversalTx(isUEADeployed_1, feeLockTxHash_1, universalPayload_1, verificationData_1) {
|
|
1248
3830
|
return tslib_1.__awaiter(this, arguments, void 0, function* (isUEADeployed, feeLockTxHash, universalPayload, verificationData, eventBuffer = []) {
|
|
1249
|
-
var _a, _b;
|
|
3831
|
+
var _a, _b, _c, _d;
|
|
1250
3832
|
const { chain, address } = this.universalSigner.account;
|
|
1251
3833
|
const { vm, chainId } = chain_1.CHAIN_INFO[chain];
|
|
1252
3834
|
const universalAccountId = {
|
|
@@ -1293,12 +3875,40 @@ class Orchestrator {
|
|
|
1293
3875
|
const txRaw = yield this.pushClient.signCosmosTx(txBody);
|
|
1294
3876
|
const tx = yield this.pushClient.broadcastCosmosTx(txRaw);
|
|
1295
3877
|
if (tx.code !== 0) {
|
|
3878
|
+
// Try to extract ethereum tx hash even from failed tx
|
|
3879
|
+
const failedEthTxHashes = (_b = (_a = tx.events) === null || _a === void 0 ? void 0 : _a.filter((e) => e.type === 'ethereum_tx').flatMap((e) => {
|
|
3880
|
+
var _a;
|
|
3881
|
+
return (_a = e.attributes) === null || _a === void 0 ? void 0 : _a.filter((attr) => attr.key === 'ethereumTxHash').map((attr) => attr.value);
|
|
3882
|
+
})) !== null && _b !== void 0 ? _b : [];
|
|
3883
|
+
// Print clean transaction failure summary
|
|
3884
|
+
console.error(`\n${'='.repeat(80)}`);
|
|
3885
|
+
console.error(`PUSH CHAIN TRANSACTION FAILED`);
|
|
3886
|
+
console.error(`${'='.repeat(80)}`);
|
|
3887
|
+
console.error(`\n--- TRANSACTION INFO ---`);
|
|
3888
|
+
console.error(`Cosmos TX Hash: ${tx.transactionHash}`);
|
|
3889
|
+
console.error(`Block Height: ${tx.height}`);
|
|
3890
|
+
console.error(`TX Code: ${tx.code} (error)`);
|
|
3891
|
+
console.error(`Gas Used: ${tx.gasUsed}`);
|
|
3892
|
+
console.error(`Gas Wanted: ${tx.gasWanted}`);
|
|
3893
|
+
if (failedEthTxHashes.length > 0) {
|
|
3894
|
+
console.error(`Ethereum TX Hash(es): ${failedEthTxHashes.join(', ')}`);
|
|
3895
|
+
}
|
|
3896
|
+
console.error(`\n--- ERROR ---`);
|
|
3897
|
+
console.error(`${tx.rawLog}`);
|
|
3898
|
+
console.error(`\n--- QUERY COMMANDS ---`);
|
|
3899
|
+
console.error(`# Query via Cosmos RPC:`);
|
|
3900
|
+
console.error(`curl -s "https://donut.rpc.push.org/tx?hash=0x${tx.transactionHash}"`);
|
|
3901
|
+
if (failedEthTxHashes.length > 0) {
|
|
3902
|
+
console.error(`\n# View on explorer:`);
|
|
3903
|
+
console.error(`https://explorer.push.org/tx/${failedEthTxHashes[0]}`);
|
|
3904
|
+
}
|
|
3905
|
+
console.error(`\n${'='.repeat(80)}\n`);
|
|
1296
3906
|
throw new Error(tx.rawLog);
|
|
1297
3907
|
}
|
|
1298
|
-
const ethTxHashes = (
|
|
3908
|
+
const ethTxHashes = (_d = (_c = tx.events) === null || _c === void 0 ? void 0 : _c.filter((e) => e.type === 'ethereum_tx').flatMap((e) => {
|
|
1299
3909
|
var _a;
|
|
1300
3910
|
return (_a = e.attributes) === null || _a === void 0 ? void 0 : _a.filter((attr) => attr.key === 'ethereumTxHash').map((attr) => attr.value);
|
|
1301
|
-
})) !== null &&
|
|
3911
|
+
})) !== null && _d !== void 0 ? _d : [];
|
|
1302
3912
|
if (ethTxHashes.length === 0) {
|
|
1303
3913
|
throw new Error('No ethereumTxHash found in transaction events');
|
|
1304
3914
|
}
|
|
@@ -1307,7 +3917,8 @@ class Orchestrator {
|
|
|
1307
3917
|
return yield this.pushClient.getTransaction(hash);
|
|
1308
3918
|
})));
|
|
1309
3919
|
// Pass eventBuffer only to the last transaction (which is the one returned to user)
|
|
1310
|
-
|
|
3920
|
+
const responses = yield Promise.all(evmTxs.map((tx, index) => this.transformToUniversalTxResponse(tx, index === evmTxs.length - 1 ? eventBuffer : [])));
|
|
3921
|
+
return responses;
|
|
1311
3922
|
});
|
|
1312
3923
|
}
|
|
1313
3924
|
/**
|
|
@@ -1318,9 +3929,68 @@ class Orchestrator {
|
|
|
1318
3929
|
*/
|
|
1319
3930
|
sendPushTx(execute_1) {
|
|
1320
3931
|
return tslib_1.__awaiter(this, arguments, void 0, function* (execute, eventBuffer = []) {
|
|
1321
|
-
//
|
|
3932
|
+
// If data is a multicall array, execute each call as a separate transaction
|
|
3933
|
+
// EOAs on Push Chain cannot process UEA_MULTICALL encoded data — they are not contracts
|
|
3934
|
+
// Nonces and gas are managed locally:
|
|
3935
|
+
// - Nonces: avoids stale nonce from estimateGas simulation on Cosmos-EVM
|
|
3936
|
+
// - Gas: avoids estimateGas failures when prior txs in the batch aren't committed yet
|
|
3937
|
+
// Each tx is confirmed before sending the next to ensure state visibility.
|
|
1322
3938
|
if (Array.isArray(execute.data)) {
|
|
1323
|
-
|
|
3939
|
+
const PUSH_CHAIN_GAS_LIMIT = BigInt(500000);
|
|
3940
|
+
const MAX_NONCE_RETRIES = 3;
|
|
3941
|
+
// Fetch nonce once before the batch using 'pending' to include mempool txs
|
|
3942
|
+
let nonce = yield this.pushClient.publicClient.getTransactionCount({
|
|
3943
|
+
address: this.universalSigner.account.address,
|
|
3944
|
+
blockTag: 'pending',
|
|
3945
|
+
});
|
|
3946
|
+
let lastTxHash = '0x';
|
|
3947
|
+
const calls = execute.data;
|
|
3948
|
+
for (let i = 0; i < calls.length; i++) {
|
|
3949
|
+
const call = calls[i];
|
|
3950
|
+
let txSent = false;
|
|
3951
|
+
for (let retry = 0; retry < MAX_NONCE_RETRIES && !txSent; retry++) {
|
|
3952
|
+
try {
|
|
3953
|
+
this.printLog(`sendPushTx — executing multicall operation ${i + 1}/${calls.length} to: ${call.to} (nonce: ${nonce})`);
|
|
3954
|
+
lastTxHash = yield this.pushClient.sendTransaction({
|
|
3955
|
+
to: call.to,
|
|
3956
|
+
data: (call.data || '0x'),
|
|
3957
|
+
value: call.value,
|
|
3958
|
+
signer: this.universalSigner,
|
|
3959
|
+
nonce,
|
|
3960
|
+
gas: PUSH_CHAIN_GAS_LIMIT,
|
|
3961
|
+
});
|
|
3962
|
+
txSent = true;
|
|
3963
|
+
}
|
|
3964
|
+
catch (err) {
|
|
3965
|
+
const msg = (err === null || err === void 0 ? void 0 : err.message) || (err === null || err === void 0 ? void 0 : err.details) || '';
|
|
3966
|
+
if (msg.includes('invalid nonce') || msg.includes('invalid sequence')) {
|
|
3967
|
+
// Re-fetch nonce from pending state and retry
|
|
3968
|
+
this.printLog(`sendPushTx — nonce mismatch on operation ${i + 1}/${calls.length} (retry ${retry + 1}/${MAX_NONCE_RETRIES}), re-fetching nonce`);
|
|
3969
|
+
nonce = yield this.pushClient.publicClient.getTransactionCount({
|
|
3970
|
+
address: this.universalSigner.account.address,
|
|
3971
|
+
blockTag: 'pending',
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
else {
|
|
3975
|
+
throw err;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
if (!txSent) {
|
|
3980
|
+
throw new Error(`sendPushTx — multicall operation ${i + 1}/${calls.length} failed after ${MAX_NONCE_RETRIES} nonce retries`);
|
|
3981
|
+
}
|
|
3982
|
+
// Wait for tx receipt and verify it succeeded before sending next tx
|
|
3983
|
+
const receipt = yield this.pushClient.publicClient.waitForTransactionReceipt({
|
|
3984
|
+
hash: lastTxHash,
|
|
3985
|
+
});
|
|
3986
|
+
if (receipt.status === 'reverted') {
|
|
3987
|
+
throw new Error(`sendPushTx — multicall operation ${i + 1}/${calls.length} reverted (to: ${call.to}, txHash: ${lastTxHash})`);
|
|
3988
|
+
}
|
|
3989
|
+
this.printLog(`sendPushTx — operation ${i + 1}/${calls.length} confirmed in block ${receipt.blockNumber}`);
|
|
3990
|
+
nonce++; // increment locally for next tx
|
|
3991
|
+
}
|
|
3992
|
+
const txResponse = yield this.pushClient.getTransaction(lastTxHash);
|
|
3993
|
+
return yield this.transformToUniversalTxResponse(txResponse, eventBuffer);
|
|
1324
3994
|
}
|
|
1325
3995
|
const txHash = yield this.pushClient.sendTransaction({
|
|
1326
3996
|
to: execute.to,
|
|
@@ -1632,7 +4302,7 @@ class Orchestrator {
|
|
|
1632
4302
|
try {
|
|
1633
4303
|
// Attempt to find UniversalTx by searching pcTx entries
|
|
1634
4304
|
// For now, we'll try to look up by the tx hash
|
|
1635
|
-
const utxResponse = yield this.pushClient.
|
|
4305
|
+
const utxResponse = yield this.pushClient.getUniversalTxByIdV2(txHash);
|
|
1636
4306
|
if (utxResponse === null || utxResponse === void 0 ? void 0 : utxResponse.universalTx) {
|
|
1637
4307
|
universalTxData = utxResponse.universalTx;
|
|
1638
4308
|
}
|
|
@@ -1665,6 +4335,442 @@ class Orchestrator {
|
|
|
1665
4335
|
return enums_1.CHAIN.PUSH_LOCALNET;
|
|
1666
4336
|
}
|
|
1667
4337
|
}
|
|
4338
|
+
// ============================================================================
|
|
4339
|
+
// Outbound Transaction Tracking
|
|
4340
|
+
// ============================================================================
|
|
4341
|
+
/**
|
|
4342
|
+
* Compute UniversalTxId for Push Chain originated outbound transactions.
|
|
4343
|
+
* Formula: keccak256("eip155:{pushChainId}:{pushChainTxHash}")
|
|
4344
|
+
*
|
|
4345
|
+
* @param pushChainTxHash - The Push Chain transaction hash
|
|
4346
|
+
* @returns The UniversalTxId (keccak256 hash)
|
|
4347
|
+
*/
|
|
4348
|
+
computeUniversalTxId(pushChainTxHash) {
|
|
4349
|
+
const pushChain = this.getPushChainForNetwork();
|
|
4350
|
+
const pushChainId = chain_1.CHAIN_INFO[pushChain].chainId;
|
|
4351
|
+
const input = `eip155:${pushChainId}:${pushChainTxHash}`;
|
|
4352
|
+
return (0, viem_1.keccak256)((0, viem_1.toBytes)(input));
|
|
4353
|
+
}
|
|
4354
|
+
/**
|
|
4355
|
+
* Extract universalsubTxId from a Push Chain transaction by fetching Cosmos events.
|
|
4356
|
+
* The universalsubTxId is found in the 'outbound_created' Cosmos event (attribute 'utx_id'),
|
|
4357
|
+
* NOT in the EVM event data.
|
|
4358
|
+
*
|
|
4359
|
+
* @param pushChainTxHash - The Push Chain transaction hash
|
|
4360
|
+
* @returns The universalsubTxId or null if not found
|
|
4361
|
+
*/
|
|
4362
|
+
extractUniversalSubTxIdFromTx(pushChainTxHash) {
|
|
4363
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
4364
|
+
var _a;
|
|
4365
|
+
this.printLog(`[extractUniversalSubTxIdFromTx] Fetching Cosmos tx for: ${pushChainTxHash}`);
|
|
4366
|
+
try {
|
|
4367
|
+
// Query Cosmos transaction to get events
|
|
4368
|
+
const cosmosTx = yield this.pushClient.getCosmosTx(pushChainTxHash);
|
|
4369
|
+
if (!(cosmosTx === null || cosmosTx === void 0 ? void 0 : cosmosTx.events)) {
|
|
4370
|
+
this.printLog(`[extractUniversalSubTxIdFromTx] No events in Cosmos tx`);
|
|
4371
|
+
return null;
|
|
4372
|
+
}
|
|
4373
|
+
// Find the 'outbound_created' event which contains utx_id
|
|
4374
|
+
for (const event of cosmosTx.events) {
|
|
4375
|
+
if (event.type === 'outbound_created') {
|
|
4376
|
+
// Find the utx_id attribute
|
|
4377
|
+
const utxIdAttr = (_a = event.attributes) === null || _a === void 0 ? void 0 : _a.find((attr) => attr.key === 'utx_id');
|
|
4378
|
+
if (utxIdAttr === null || utxIdAttr === void 0 ? void 0 : utxIdAttr.value) {
|
|
4379
|
+
// The utx_id is stored without 0x prefix in Cosmos events
|
|
4380
|
+
const universalsubTxId = utxIdAttr.value.startsWith('0x')
|
|
4381
|
+
? utxIdAttr.value
|
|
4382
|
+
: `0x${utxIdAttr.value}`;
|
|
4383
|
+
this.printLog(`[extractUniversalSubTxIdFromTx] Found universalsubTxId from outbound_created event: ${universalsubTxId}`);
|
|
4384
|
+
return universalsubTxId;
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
this.printLog(`[extractUniversalSubTxIdFromTx] No outbound_created event found`);
|
|
4389
|
+
return null;
|
|
4390
|
+
}
|
|
4391
|
+
catch (error) {
|
|
4392
|
+
this.printLog(`[extractUniversalSubTxIdFromTx] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4393
|
+
return null;
|
|
4394
|
+
}
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
/**
|
|
4398
|
+
* Extract ALL universalSubTxIds from a Push Chain transaction.
|
|
4399
|
+
* For cascaded transactions, a single Push Chain tx may emit multiple
|
|
4400
|
+
* outbound_created events, each with its own utx_id.
|
|
4401
|
+
*
|
|
4402
|
+
* @param pushChainTxHash - The Push Chain transaction hash
|
|
4403
|
+
* @returns Array of universalSubTxIds (may be empty)
|
|
4404
|
+
*/
|
|
4405
|
+
extractAllUniversalSubTxIds(pushChainTxHash) {
|
|
4406
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
4407
|
+
var _a;
|
|
4408
|
+
this.printLog(`[extractAllUniversalSubTxIds] Fetching Cosmos tx for: ${pushChainTxHash}`);
|
|
4409
|
+
try {
|
|
4410
|
+
const cosmosTx = yield this.pushClient.getCosmosTx(pushChainTxHash);
|
|
4411
|
+
if (!(cosmosTx === null || cosmosTx === void 0 ? void 0 : cosmosTx.events)) {
|
|
4412
|
+
this.printLog(`[extractAllUniversalSubTxIds] No events in Cosmos tx`);
|
|
4413
|
+
return [];
|
|
4414
|
+
}
|
|
4415
|
+
const subTxIds = [];
|
|
4416
|
+
for (const event of cosmosTx.events) {
|
|
4417
|
+
if (event.type === 'outbound_created') {
|
|
4418
|
+
const utxIdAttr = (_a = event.attributes) === null || _a === void 0 ? void 0 : _a.find((attr) => attr.key === 'utx_id');
|
|
4419
|
+
if (utxIdAttr === null || utxIdAttr === void 0 ? void 0 : utxIdAttr.value) {
|
|
4420
|
+
const id = utxIdAttr.value.startsWith('0x')
|
|
4421
|
+
? utxIdAttr.value
|
|
4422
|
+
: `0x${utxIdAttr.value}`;
|
|
4423
|
+
subTxIds.push(id);
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
this.printLog(`[extractAllUniversalSubTxIds] Found ${subTxIds.length} sub-tx IDs: ${subTxIds.join(', ')}`);
|
|
4428
|
+
return subTxIds;
|
|
4429
|
+
}
|
|
4430
|
+
catch (error) {
|
|
4431
|
+
this.printLog(`[extractAllUniversalSubTxIds] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4432
|
+
return [];
|
|
4433
|
+
}
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
/**
|
|
4437
|
+
* Convert CAIP-2 namespace (e.g., "eip155:97") to CHAIN enum
|
|
4438
|
+
*
|
|
4439
|
+
* @param namespace - CAIP-2 chain namespace string
|
|
4440
|
+
* @returns CHAIN enum value or null if not found
|
|
4441
|
+
*/
|
|
4442
|
+
chainFromNamespace(namespace) {
|
|
4443
|
+
for (const [chainKey, info] of Object.entries(chain_1.CHAIN_INFO)) {
|
|
4444
|
+
const expected = `${chain_1.VM_NAMESPACE[info.vm]}:${info.chainId}`;
|
|
4445
|
+
if (expected === namespace) {
|
|
4446
|
+
return chainKey;
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
return null;
|
|
4450
|
+
}
|
|
4451
|
+
/**
|
|
4452
|
+
* Wait for outbound transaction to complete and return external chain details.
|
|
4453
|
+
* @internal Used by .wait() for outbound routes - not part of public API.
|
|
4454
|
+
* Uses polling with configurable initial wait, interval, and timeout.
|
|
4455
|
+
*
|
|
4456
|
+
* Default strategy: 30s initial wait, then poll every 5s, 120s total timeout.
|
|
4457
|
+
*
|
|
4458
|
+
* @param pushChainTxHash - The Push Chain transaction hash
|
|
4459
|
+
* @param options - Polling configuration options
|
|
4460
|
+
* @returns External chain tx details
|
|
4461
|
+
* @throws Error on timeout
|
|
4462
|
+
*/
|
|
4463
|
+
waitForOutboundTx(pushChainTxHash_1) {
|
|
4464
|
+
return tslib_1.__awaiter(this, arguments, void 0, function* (pushChainTxHash, options = {}) {
|
|
4465
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
4466
|
+
const { initialWaitMs = Orchestrator.OUTBOUND_INITIAL_WAIT_MS, pollingIntervalMs = Orchestrator.OUTBOUND_POLL_INTERVAL_MS, timeout = Orchestrator.OUTBOUND_MAX_TIMEOUT_MS, progressHook, _resolvedSubTxId, _expectedDestinationChain, } = options;
|
|
4467
|
+
// Terminal failure states — fail fast instead of polling until timeout
|
|
4468
|
+
const TERMINAL_FAILURE_STATES = new Set([
|
|
4469
|
+
types_1.UniversalTxStatus.OUTBOUND_FAILED,
|
|
4470
|
+
types_1.UniversalTxStatus.PC_EXECUTED_FAILED,
|
|
4471
|
+
types_1.UniversalTxStatus.CANCELED,
|
|
4472
|
+
]);
|
|
4473
|
+
const startTime = Date.now();
|
|
4474
|
+
this.printLog(`[waitForOutboundTx] Starting | txHash: ${pushChainTxHash} | initialWait: ${initialWaitMs}ms | pollInterval: ${pollingIntervalMs}ms | timeout: ${timeout}ms`);
|
|
4475
|
+
// Emit initial waiting status
|
|
4476
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'waiting', elapsed: 0 });
|
|
4477
|
+
// Initial wait before first poll
|
|
4478
|
+
this.printLog(`[waitForOutboundTx] Initial wait of ${initialWaitMs}ms...`);
|
|
4479
|
+
yield new Promise((resolve) => setTimeout(resolve, initialWaitMs));
|
|
4480
|
+
// Start polling
|
|
4481
|
+
this.printLog(`[waitForOutboundTx] Initial wait done. Starting polling. Elapsed: ${Date.now() - startTime}ms`);
|
|
4482
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'polling', elapsed: Date.now() - startTime });
|
|
4483
|
+
// Cache the universalSubTxId after first extraction to avoid redundant receipt fetches.
|
|
4484
|
+
// If a pre-resolved ID was provided (cascade per-hop tracking), use it directly.
|
|
4485
|
+
let cachedUniversalSubTxId = _resolvedSubTxId;
|
|
4486
|
+
let pollCount = 0;
|
|
4487
|
+
while (Date.now() - startTime < timeout) {
|
|
4488
|
+
pollCount++;
|
|
4489
|
+
const pollStart = Date.now();
|
|
4490
|
+
this.printLog(`[waitForOutboundTx] Poll #${pollCount} | Elapsed: ${pollStart - startTime}ms / ${timeout}ms`);
|
|
4491
|
+
// First poll: extract the ID. Subsequent polls: reuse cached ID.
|
|
4492
|
+
if (!cachedUniversalSubTxId) {
|
|
4493
|
+
cachedUniversalSubTxId = (_a = (yield this.extractUniversalSubTxIdFromTx(pushChainTxHash))) !== null && _a !== void 0 ? _a : undefined;
|
|
4494
|
+
if (!cachedUniversalSubTxId) {
|
|
4495
|
+
cachedUniversalSubTxId = this.computeUniversalTxId(pushChainTxHash);
|
|
4496
|
+
}
|
|
4497
|
+
this.printLog(`[waitForOutboundTx] Extracted & cached universalSubTxId: ${cachedUniversalSubTxId}`);
|
|
4498
|
+
}
|
|
4499
|
+
// Query with cached ID
|
|
4500
|
+
const queryId = cachedUniversalSubTxId.startsWith('0x')
|
|
4501
|
+
? cachedUniversalSubTxId.slice(2)
|
|
4502
|
+
: cachedUniversalSubTxId;
|
|
4503
|
+
try {
|
|
4504
|
+
const utxResponse = yield this.pushClient.getUniversalTxByIdV2(queryId);
|
|
4505
|
+
const statusNum = (_b = utxResponse === null || utxResponse === void 0 ? void 0 : utxResponse.universalTx) === null || _b === void 0 ? void 0 : _b.universalStatus;
|
|
4506
|
+
const statusName = (_c = types_1.UniversalTxStatus[statusNum]) !== null && _c !== void 0 ? _c : statusNum;
|
|
4507
|
+
const outbounds = ((_d = utxResponse === null || utxResponse === void 0 ? void 0 : utxResponse.universalTx) === null || _d === void 0 ? void 0 : _d.outboundTx) || [];
|
|
4508
|
+
this.printLog(`[waitForOutboundTx] Poll #${pollCount} | status: ${statusNum} (${statusName}) | outboundTx count: ${outbounds.length} | first txHash: '${((_f = (_e = outbounds[0]) === null || _e === void 0 ? void 0 : _e.observedTx) === null || _f === void 0 ? void 0 : _f.txHash) || ''}' | first dest: '${((_g = outbounds[0]) === null || _g === void 0 ? void 0 : _g.destinationChain) || ''}'`);
|
|
4509
|
+
// Check for terminal failure states — fail fast
|
|
4510
|
+
if (TERMINAL_FAILURE_STATES.has(statusNum)) {
|
|
4511
|
+
this.printLog(`[waitForOutboundTx] Terminal failure state: ${statusName}`);
|
|
4512
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'failed', elapsed: Date.now() - startTime });
|
|
4513
|
+
throw new Error(`Outbound transaction failed with status ${statusName}. Push Chain TX: ${pushChainTxHash}.`);
|
|
4514
|
+
}
|
|
4515
|
+
// Iterate V2 outbound array
|
|
4516
|
+
for (const ob of outbounds) {
|
|
4517
|
+
// Fail fast on per-outbound REVERTED status
|
|
4518
|
+
if (ob.outboundStatus === types_2.OutboundStatus.REVERTED) {
|
|
4519
|
+
this.printLog(`[waitForOutboundTx] Outbound to ${ob.destinationChain} REVERTED`);
|
|
4520
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'failed', elapsed: Date.now() - startTime });
|
|
4521
|
+
throw new Error(`Outbound to ${ob.destinationChain} reverted: ${((_h = ob.observedTx) === null || _h === void 0 ? void 0 : _h.errorMsg) || 'Unknown'}. Push Chain TX: ${pushChainTxHash}.`);
|
|
4522
|
+
}
|
|
4523
|
+
if ((_j = ob.observedTx) === null || _j === void 0 ? void 0 : _j.txHash) {
|
|
4524
|
+
// If a destination chain filter is set, skip outbound entries that don't match
|
|
4525
|
+
if (_expectedDestinationChain && ob.destinationChain !== _expectedDestinationChain) {
|
|
4526
|
+
this.printLog(`[waitForOutboundTx] Poll #${pollCount} | outbound chain '${ob.destinationChain}' does not match expected '${_expectedDestinationChain}', skipping`);
|
|
4527
|
+
continue;
|
|
4528
|
+
}
|
|
4529
|
+
const chain = this.chainFromNamespace(ob.destinationChain);
|
|
4530
|
+
if (chain) {
|
|
4531
|
+
const explorerBaseUrl = (_k = chain_1.CHAIN_INFO[chain]) === null || _k === void 0 ? void 0 : _k.explorerUrl;
|
|
4532
|
+
const isSvm = ((_l = chain_1.CHAIN_INFO[chain]) === null || _l === void 0 ? void 0 : _l.vm) === enums_1.VM.SVM;
|
|
4533
|
+
// For SVM chains, convert hex txHash to base58 and append cluster param
|
|
4534
|
+
let displayTxHash = ob.observedTx.txHash;
|
|
4535
|
+
let explorerUrl = '';
|
|
4536
|
+
if (isSvm && ob.observedTx.txHash.startsWith('0x')) {
|
|
4537
|
+
const bytes = new Uint8Array(Buffer.from(ob.observedTx.txHash.slice(2), 'hex'));
|
|
4538
|
+
displayTxHash = anchor_1.utils.bytes.bs58.encode(bytes);
|
|
4539
|
+
const cluster = chain === enums_1.CHAIN.SOLANA_DEVNET ? '?cluster=devnet'
|
|
4540
|
+
: chain === enums_1.CHAIN.SOLANA_TESTNET ? '?cluster=testnet' : '';
|
|
4541
|
+
explorerUrl = explorerBaseUrl ? `${explorerBaseUrl}/tx/${displayTxHash}${cluster}` : '';
|
|
4542
|
+
}
|
|
4543
|
+
else {
|
|
4544
|
+
explorerUrl = explorerBaseUrl ? `${explorerBaseUrl}/tx/${ob.observedTx.txHash}` : '';
|
|
4545
|
+
}
|
|
4546
|
+
const details = {
|
|
4547
|
+
externalTxHash: ob.observedTx.txHash,
|
|
4548
|
+
destinationChain: chain,
|
|
4549
|
+
explorerUrl,
|
|
4550
|
+
recipient: ob.recipient,
|
|
4551
|
+
amount: ob.amount,
|
|
4552
|
+
assetAddr: ob.externalAssetAddr,
|
|
4553
|
+
};
|
|
4554
|
+
this.printLog(`[waitForOutboundTx] FOUND on poll #${pollCount} | elapsed: ${Date.now() - startTime}ms | externalTxHash: ${details.externalTxHash}`);
|
|
4555
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'found', elapsed: Date.now() - startTime });
|
|
4556
|
+
return details;
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
catch (error) {
|
|
4562
|
+
// Re-throw terminal failure and reverted errors
|
|
4563
|
+
if (error instanceof Error && (error.message.includes('Outbound transaction failed') || error.message.includes('reverted'))) {
|
|
4564
|
+
throw error;
|
|
4565
|
+
}
|
|
4566
|
+
this.printLog(`[waitForOutboundTx] Poll #${pollCount} ERROR: ${error instanceof Error ? error.message : String(error)}`);
|
|
4567
|
+
}
|
|
4568
|
+
this.printLog(`[waitForOutboundTx] Poll #${pollCount} not ready yet (${Date.now() - pollStart}ms). Waiting ${pollingIntervalMs}ms...`);
|
|
4569
|
+
// Wait before next poll
|
|
4570
|
+
yield new Promise((resolve) => setTimeout(resolve, pollingIntervalMs));
|
|
4571
|
+
}
|
|
4572
|
+
this.printLog(`[waitForOutboundTx] TIMEOUT after ${pollCount} polls | elapsed: ${Date.now() - startTime}ms`);
|
|
4573
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'timeout', elapsed: Date.now() - startTime });
|
|
4574
|
+
throw new Error(`Timeout waiting for outbound transaction. Push Chain TX: ${pushChainTxHash}. ` +
|
|
4575
|
+
`Timeout: ${timeout}ms. The relay may still be processing.`);
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Tracks ALL outbound transactions for a cascade with multiple outbound hops
|
|
4580
|
+
* (e.g., BNB + Solana). Uses V2 API which returns outboundTx[] with per-outbound
|
|
4581
|
+
* status tracking, matching each outbound to the correct hop by destination chain.
|
|
4582
|
+
*/
|
|
4583
|
+
waitForAllOutboundTxsV2(pushChainTxHash, outboundHops, options) {
|
|
4584
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
4585
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
4586
|
+
const { initialWaitMs, pollingIntervalMs, timeout, progressHook } = options;
|
|
4587
|
+
const startTime = Date.now();
|
|
4588
|
+
// Build a map: CAIP-2 namespace -> hop(s) for matching outbound entries
|
|
4589
|
+
const chainToHops = new Map();
|
|
4590
|
+
for (const hop of outboundHops) {
|
|
4591
|
+
const chainInfo = chain_1.CHAIN_INFO[hop.executionChain];
|
|
4592
|
+
if (chainInfo) {
|
|
4593
|
+
const namespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
|
|
4594
|
+
const existing = chainToHops.get(namespace) || [];
|
|
4595
|
+
existing.push(hop);
|
|
4596
|
+
chainToHops.set(namespace, existing);
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
const expectedChains = [...chainToHops.keys()];
|
|
4600
|
+
this.printLog(`[waitForAllOutboundTxsV2] Starting | txHash: ${pushChainTxHash} | expectedChains: ${expectedChains.join(', ')} | timeout: ${timeout}ms`);
|
|
4601
|
+
// Emit initial waiting status for all outbound hops
|
|
4602
|
+
for (const hop of outboundHops) {
|
|
4603
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({
|
|
4604
|
+
hopIndex: hop.hopIndex,
|
|
4605
|
+
route: hop.route,
|
|
4606
|
+
chain: hop.executionChain,
|
|
4607
|
+
status: 'waiting',
|
|
4608
|
+
});
|
|
4609
|
+
}
|
|
4610
|
+
// Initial wait before first poll
|
|
4611
|
+
const waitMs = Math.min(initialWaitMs, timeout);
|
|
4612
|
+
this.printLog(`[waitForAllOutboundTxsV2] Initial wait of ${waitMs}ms...`);
|
|
4613
|
+
yield new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
4614
|
+
// Emit polling status for all hops
|
|
4615
|
+
for (const hop of outboundHops) {
|
|
4616
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({
|
|
4617
|
+
hopIndex: hop.hopIndex,
|
|
4618
|
+
route: hop.route,
|
|
4619
|
+
chain: hop.executionChain,
|
|
4620
|
+
status: 'polling',
|
|
4621
|
+
});
|
|
4622
|
+
}
|
|
4623
|
+
// Extract sub-tx ID for V2 query
|
|
4624
|
+
let cachedQueryId;
|
|
4625
|
+
let pollCount = 0;
|
|
4626
|
+
let consecutiveErrors = 0;
|
|
4627
|
+
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
4628
|
+
while (Date.now() - startTime < timeout) {
|
|
4629
|
+
pollCount++;
|
|
4630
|
+
const elapsed = Date.now() - startTime;
|
|
4631
|
+
this.printLog(`[waitForAllOutboundTxsV2] Poll #${pollCount} | Elapsed: ${elapsed}ms / ${timeout}ms`);
|
|
4632
|
+
// Resolve query ID on first poll
|
|
4633
|
+
if (!cachedQueryId) {
|
|
4634
|
+
const allSubTxIds = yield this.extractAllUniversalSubTxIds(pushChainTxHash);
|
|
4635
|
+
const subTxId = allSubTxIds.length > 0 ? allSubTxIds[0] : this.computeUniversalTxId(pushChainTxHash);
|
|
4636
|
+
cachedQueryId = subTxId.startsWith('0x') ? subTxId.slice(2) : subTxId;
|
|
4637
|
+
this.printLog(`[waitForAllOutboundTxsV2] Resolved queryId: ${cachedQueryId}`);
|
|
4638
|
+
}
|
|
4639
|
+
try {
|
|
4640
|
+
const v2Response = yield this.pushClient.getUniversalTxByIdV2(cachedQueryId);
|
|
4641
|
+
consecutiveErrors = 0; // Reset on successful RPC call
|
|
4642
|
+
const utx = v2Response === null || v2Response === void 0 ? void 0 : v2Response.universalTx;
|
|
4643
|
+
const statusNum = utx === null || utx === void 0 ? void 0 : utx.universalStatus;
|
|
4644
|
+
const statusName = (_a = types_1.UniversalTxStatus[statusNum]) !== null && _a !== void 0 ? _a : statusNum;
|
|
4645
|
+
this.printLog(`[waitForAllOutboundTxsV2] Poll #${pollCount} | status: ${statusNum} (${statusName}) | outboundTx count: ${(_c = (_b = utx === null || utx === void 0 ? void 0 : utx.outboundTx) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0}`);
|
|
4646
|
+
if ((_d = utx === null || utx === void 0 ? void 0 : utx.outboundTx) === null || _d === void 0 ? void 0 : _d.length) {
|
|
4647
|
+
for (const ob of utx.outboundTx) {
|
|
4648
|
+
const destChain = ob.destinationChain;
|
|
4649
|
+
const hopsForChain = chainToHops.get(destChain);
|
|
4650
|
+
if (!hopsForChain)
|
|
4651
|
+
continue;
|
|
4652
|
+
const unconfirmedForChain = hopsForChain.filter((h) => h.status !== 'confirmed' && h.status !== 'failed');
|
|
4653
|
+
if (unconfirmedForChain.length === 0)
|
|
4654
|
+
continue;
|
|
4655
|
+
// Fail fast on per-outbound REVERTED
|
|
4656
|
+
if (ob.outboundStatus === types_2.OutboundStatus.REVERTED) {
|
|
4657
|
+
for (const hop of unconfirmedForChain) {
|
|
4658
|
+
hop.status = 'failed';
|
|
4659
|
+
this.printLog(`[waitForAllOutboundTxsV2] Outbound to ${destChain} REVERTED | hop ${hop.hopIndex} | error: ${((_e = ob.observedTx) === null || _e === void 0 ? void 0 : _e.errorMsg) || 'Unknown'}`);
|
|
4660
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({
|
|
4661
|
+
hopIndex: hop.hopIndex,
|
|
4662
|
+
route: hop.route,
|
|
4663
|
+
chain: hop.executionChain,
|
|
4664
|
+
status: 'failed',
|
|
4665
|
+
});
|
|
4666
|
+
}
|
|
4667
|
+
return { success: false, failedAt: unconfirmedForChain[0].hopIndex };
|
|
4668
|
+
}
|
|
4669
|
+
// Check for OBSERVED with txHash
|
|
4670
|
+
const externalTxHash = (_f = ob.observedTx) === null || _f === void 0 ? void 0 : _f.txHash;
|
|
4671
|
+
if (externalTxHash && (ob.outboundStatus === types_2.OutboundStatus.OBSERVED || ob.outboundStatus === 0)) {
|
|
4672
|
+
const chain = this.chainFromNamespace(destChain);
|
|
4673
|
+
let explorerUrl = '';
|
|
4674
|
+
if (chain && externalTxHash) {
|
|
4675
|
+
const explorerBaseUrl = (_g = chain_1.CHAIN_INFO[chain]) === null || _g === void 0 ? void 0 : _g.explorerUrl;
|
|
4676
|
+
const isSvm = ((_h = chain_1.CHAIN_INFO[chain]) === null || _h === void 0 ? void 0 : _h.vm) === enums_1.VM.SVM;
|
|
4677
|
+
if (isSvm && externalTxHash.startsWith('0x')) {
|
|
4678
|
+
const bytes = new Uint8Array(Buffer.from(externalTxHash.slice(2), 'hex'));
|
|
4679
|
+
const base58Hash = anchor_1.utils.bytes.bs58.encode(bytes);
|
|
4680
|
+
const cluster = chain === enums_1.CHAIN.SOLANA_DEVNET ? '?cluster=devnet'
|
|
4681
|
+
: chain === enums_1.CHAIN.SOLANA_TESTNET ? '?cluster=testnet' : '';
|
|
4682
|
+
explorerUrl = explorerBaseUrl ? `${explorerBaseUrl}/tx/${base58Hash}${cluster}` : '';
|
|
4683
|
+
}
|
|
4684
|
+
else {
|
|
4685
|
+
explorerUrl = explorerBaseUrl ? `${explorerBaseUrl}/tx/${externalTxHash}` : '';
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
for (const hop of unconfirmedForChain) {
|
|
4689
|
+
hop.status = 'confirmed';
|
|
4690
|
+
hop.txHash = externalTxHash;
|
|
4691
|
+
hop.outboundDetails = {
|
|
4692
|
+
externalTxHash,
|
|
4693
|
+
destinationChain: chain || hop.executionChain,
|
|
4694
|
+
explorerUrl,
|
|
4695
|
+
recipient: ob.recipient,
|
|
4696
|
+
amount: ob.amount,
|
|
4697
|
+
assetAddr: ob.externalAssetAddr,
|
|
4698
|
+
};
|
|
4699
|
+
this.printLog(`[waitForAllOutboundTxsV2] FOUND outbound for ${destChain} | hop ${hop.hopIndex} | externalTxHash: ${externalTxHash}`);
|
|
4700
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({
|
|
4701
|
+
hopIndex: hop.hopIndex,
|
|
4702
|
+
route: hop.route,
|
|
4703
|
+
chain: hop.executionChain,
|
|
4704
|
+
status: 'confirmed',
|
|
4705
|
+
txHash: externalTxHash,
|
|
4706
|
+
});
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
// Check if all hops are now confirmed
|
|
4712
|
+
if (outboundHops.every((h) => h.status === 'confirmed')) {
|
|
4713
|
+
this.printLog(`[waitForAllOutboundTxsV2] All ${outboundHops.length} hops confirmed via V2`);
|
|
4714
|
+
return { success: true };
|
|
4715
|
+
}
|
|
4716
|
+
// If PC_EXECUTED_SUCCESS but some hops still unresolved, check outbound status.
|
|
4717
|
+
// Only auto-confirm if there are NO pending outbound txs (status=1 with empty hash).
|
|
4718
|
+
// Pending outbounds are still in flight on the relay — keep polling for their hashes.
|
|
4719
|
+
if (statusNum === types_1.UniversalTxStatus.PC_EXECUTED_SUCCESS) {
|
|
4720
|
+
const stillUnresolved = outboundHops.filter((h) => h.status !== 'confirmed');
|
|
4721
|
+
if (stillUnresolved.length > 0) {
|
|
4722
|
+
const hasPendingOutbound = (_j = utx === null || utx === void 0 ? void 0 : utx.outboundTx) === null || _j === void 0 ? void 0 : _j.some((ob) => {
|
|
4723
|
+
var _a;
|
|
4724
|
+
return ob.outboundStatus === types_2.OutboundStatus.PENDING &&
|
|
4725
|
+
(!((_a = ob.observedTx) === null || _a === void 0 ? void 0 : _a.txHash) || ob.observedTx.txHash === 'EMPTY');
|
|
4726
|
+
});
|
|
4727
|
+
if (!hasPendingOutbound) {
|
|
4728
|
+
// No pending outbounds — safe to auto-confirm remaining hops
|
|
4729
|
+
for (const hop of stillUnresolved) {
|
|
4730
|
+
hop.status = 'confirmed';
|
|
4731
|
+
this.printLog(`[waitForAllOutboundTxsV2] Auto-confirmed hop ${hop.hopIndex} (${hop.executionChain}) based on PC_EXECUTED_SUCCESS (no pending outbounds)`);
|
|
4732
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({
|
|
4733
|
+
hopIndex: hop.hopIndex,
|
|
4734
|
+
route: hop.route,
|
|
4735
|
+
chain: hop.executionChain,
|
|
4736
|
+
status: 'confirmed',
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4739
|
+
return { success: true };
|
|
4740
|
+
}
|
|
4741
|
+
// Pending outbound txs still in flight — continue polling
|
|
4742
|
+
this.printLog(`[waitForAllOutboundTxsV2] ${stillUnresolved.length} hop(s) unresolved, pending outbound txs in flight — continuing to poll`);
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
catch (error) {
|
|
4747
|
+
consecutiveErrors++;
|
|
4748
|
+
this.printLog(`[waitForAllOutboundTxsV2] Poll #${pollCount} ERROR (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}): ${error instanceof Error ? error.message : String(error)}`);
|
|
4749
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
4750
|
+
this.printLog(`[waitForAllOutboundTxsV2] Aborting — ${MAX_CONSECUTIVE_ERRORS} consecutive RPC errors`);
|
|
4751
|
+
break;
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
yield new Promise((resolve) => setTimeout(resolve, pollingIntervalMs));
|
|
4755
|
+
}
|
|
4756
|
+
// Timeout: fail any unresolved hops
|
|
4757
|
+
const timedOutHops = outboundHops.filter((h) => h.status !== 'confirmed');
|
|
4758
|
+
if (timedOutHops.length > 0) {
|
|
4759
|
+
this.printLog(`[waitForAllOutboundTxsV2] TIMEOUT after ${pollCount} polls | ${timedOutHops.length} hop(s) unresolved`);
|
|
4760
|
+
for (const hop of timedOutHops) {
|
|
4761
|
+
hop.status = 'failed';
|
|
4762
|
+
progressHook === null || progressHook === void 0 ? void 0 : progressHook({
|
|
4763
|
+
hopIndex: hop.hopIndex,
|
|
4764
|
+
route: hop.route,
|
|
4765
|
+
chain: hop.executionChain,
|
|
4766
|
+
status: 'failed',
|
|
4767
|
+
});
|
|
4768
|
+
}
|
|
4769
|
+
return { success: false, failedAt: timedOutHops[0].hopIndex };
|
|
4770
|
+
}
|
|
4771
|
+
return { success: true };
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
1668
4774
|
/**
|
|
1669
4775
|
* Computes UEA for given UniversalAccount
|
|
1670
4776
|
* @dev - This fn calls a view fn of Factory Contract
|
|
@@ -1771,38 +4877,34 @@ class Orchestrator {
|
|
|
1771
4877
|
throw new Error('Token address is required for bridge path');
|
|
1772
4878
|
}
|
|
1773
4879
|
const isNative = mechanism === 'native' || execute.funds.token.symbol === 'SOL';
|
|
1774
|
-
const
|
|
1775
|
-
fundRecipient: userPk,
|
|
1776
|
-
revertMsg: Buffer.from([]),
|
|
1777
|
-
};
|
|
4880
|
+
const { feeVaultPda, protocolFeeLamports } = yield this._getSvmProtocolFee(svmClient, programId);
|
|
1778
4881
|
// Compute signature for universal payload on SVM
|
|
1779
4882
|
const ueaAddressSvm = this.computeUEAOffchain();
|
|
1780
4883
|
const ueaVersion = yield this.fetchUEAVersion();
|
|
1781
4884
|
const svmSignature = yield this.signUniversalPayload(universalPayload, ueaAddressSvm, ueaVersion);
|
|
1782
4885
|
if (isNative) {
|
|
1783
4886
|
// Native SOL as bridge + gas
|
|
1784
|
-
const [whitelistPdaLocal] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('whitelist')], programId);
|
|
1785
4887
|
const [tokenRateLimitPda] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('rate_limit'), web3_js_1.PublicKey.default.toBuffer()], programId);
|
|
1786
4888
|
const nativeReq = this._buildSvmUniversalTxRequest({
|
|
1787
4889
|
recipient: Array.from(Buffer.alloc(20, 0)),
|
|
1788
4890
|
token: web3_js_1.PublicKey.default,
|
|
1789
4891
|
amount: bridgeAmount,
|
|
1790
4892
|
payload: Uint8Array.from(this.encodeUniversalPayloadSvm(universalPayload)),
|
|
1791
|
-
|
|
4893
|
+
revertRecipient: userPk,
|
|
1792
4894
|
signatureData: svmSignature,
|
|
1793
4895
|
});
|
|
1794
4896
|
return yield svmClient.writeContract({
|
|
1795
4897
|
abi: abi_1.SVM_GATEWAY_IDL,
|
|
1796
4898
|
address: programId.toBase58(),
|
|
1797
4899
|
functionName: 'sendUniversalTx',
|
|
1798
|
-
args: [nativeReq, nativeAmount],
|
|
4900
|
+
args: [nativeReq, nativeAmount + protocolFeeLamports],
|
|
1799
4901
|
signer: this.universalSigner,
|
|
1800
4902
|
accounts: {
|
|
1801
4903
|
config: configPda,
|
|
1802
4904
|
vault: vaultPda,
|
|
1803
|
-
|
|
1804
|
-
userTokenAccount: vaultPda,
|
|
1805
|
-
gatewayTokenAccount: vaultPda,
|
|
4905
|
+
feeVault: feeVaultPda,
|
|
4906
|
+
userTokenAccount: vaultPda,
|
|
4907
|
+
gatewayTokenAccount: vaultPda,
|
|
1806
4908
|
user: userPk,
|
|
1807
4909
|
priceUpdate: priceUpdatePk,
|
|
1808
4910
|
tokenProgram: new web3_js_1.PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
|
|
@@ -1822,26 +4924,25 @@ class Orchestrator {
|
|
|
1822
4924
|
const ASSOCIATED_TOKEN_PROGRAM_ID = new web3_js_1.PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
|
|
1823
4925
|
const userAta = web3_js_1.PublicKey.findProgramAddressSync([userPk.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mintPk.toBuffer()], ASSOCIATED_TOKEN_PROGRAM_ID)[0];
|
|
1824
4926
|
const vaultAta = web3_js_1.PublicKey.findProgramAddressSync([vaultPda.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mintPk.toBuffer()], ASSOCIATED_TOKEN_PROGRAM_ID)[0];
|
|
1825
|
-
const [whitelistPdaLocal] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('whitelist')], programId);
|
|
1826
4927
|
const [tokenRateLimitPda] = web3_js_1.PublicKey.findProgramAddressSync([(0, viem_1.stringToBytes)('rate_limit'), mintPk.toBuffer()], programId);
|
|
1827
4928
|
const splReq = this._buildSvmUniversalTxRequest({
|
|
1828
4929
|
recipient: Array.from(Buffer.alloc(20, 0)),
|
|
1829
4930
|
token: mintPk,
|
|
1830
4931
|
amount: bridgeAmount,
|
|
1831
4932
|
payload: Uint8Array.from(this.encodeUniversalPayloadSvm(universalPayload)),
|
|
1832
|
-
|
|
4933
|
+
revertRecipient: userPk,
|
|
1833
4934
|
signatureData: svmSignature,
|
|
1834
4935
|
});
|
|
1835
4936
|
return yield svmClient.writeContract({
|
|
1836
4937
|
abi: abi_1.SVM_GATEWAY_IDL,
|
|
1837
4938
|
address: programId.toBase58(),
|
|
1838
4939
|
functionName: 'sendUniversalTx',
|
|
1839
|
-
args: [splReq, nativeAmount],
|
|
4940
|
+
args: [splReq, nativeAmount + protocolFeeLamports],
|
|
1840
4941
|
signer: this.universalSigner,
|
|
1841
4942
|
accounts: {
|
|
1842
4943
|
config: configPda,
|
|
1843
4944
|
vault: vaultPda,
|
|
1844
|
-
|
|
4945
|
+
feeVault: feeVaultPda,
|
|
1845
4946
|
userTokenAccount: userAta,
|
|
1846
4947
|
gatewayTokenAccount: vaultAta,
|
|
1847
4948
|
user: userPk,
|
|
@@ -2563,6 +5664,7 @@ class Orchestrator {
|
|
|
2563
5664
|
// 6. Utilities
|
|
2564
5665
|
wait: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
2565
5666
|
// Use trackTransaction with registered hook if available
|
|
5667
|
+
let baseReceipt;
|
|
2566
5668
|
if (registeredProgressHook) {
|
|
2567
5669
|
const trackedResponse = yield this.trackTransaction(tx.hash, {
|
|
2568
5670
|
waitForCompletion: true,
|
|
@@ -2570,10 +5672,31 @@ class Orchestrator {
|
|
|
2570
5672
|
});
|
|
2571
5673
|
// Get receipt from the tracked response
|
|
2572
5674
|
const receipt = yield tx.wait();
|
|
2573
|
-
|
|
5675
|
+
baseReceipt = this.transformToUniversalTxReceipt(receipt, trackedResponse);
|
|
5676
|
+
}
|
|
5677
|
+
else {
|
|
5678
|
+
const receipt = yield tx.wait();
|
|
5679
|
+
baseReceipt = this.transformToUniversalTxReceipt(receipt, universalTxResponse);
|
|
2574
5680
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
5681
|
+
// If outbound route (Route 2: UOA → CEA, Route 3: CEA → Push), poll for external chain details
|
|
5682
|
+
if (universalTxResponse.route === route_detector_1.TransactionRoute.UOA_TO_CEA ||
|
|
5683
|
+
universalTxResponse.route === route_detector_1.TransactionRoute.CEA_TO_PUSH) {
|
|
5684
|
+
try {
|
|
5685
|
+
const outboundDetails = yield this.waitForOutboundTx(tx.hash, {
|
|
5686
|
+
initialWaitMs: Orchestrator.OUTBOUND_INITIAL_WAIT_MS,
|
|
5687
|
+
pollingIntervalMs: Orchestrator.OUTBOUND_POLL_INTERVAL_MS,
|
|
5688
|
+
timeout: Orchestrator.OUTBOUND_MAX_TIMEOUT_MS,
|
|
5689
|
+
});
|
|
5690
|
+
// Merge external chain details into receipt
|
|
5691
|
+
baseReceipt = Object.assign(Object.assign({}, baseReceipt), { externalTxHash: outboundDetails.externalTxHash, externalChain: outboundDetails.destinationChain, externalExplorerUrl: outboundDetails.explorerUrl, externalRecipient: outboundDetails.recipient, externalAmount: outboundDetails.amount, externalAssetAddr: outboundDetails.assetAddr });
|
|
5692
|
+
}
|
|
5693
|
+
catch (error) {
|
|
5694
|
+
// Outbound polling timed out - return partial receipt (don't throw)
|
|
5695
|
+
// Push Chain tx succeeded, external tracking can be retried later
|
|
5696
|
+
this.printLog(`[wait] External chain tracking failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
return baseReceipt;
|
|
2577
5700
|
}),
|
|
2578
5701
|
progressHook: (callback) => {
|
|
2579
5702
|
registeredProgressHook = callback;
|
|
@@ -2759,12 +5882,12 @@ class Orchestrator {
|
|
|
2759
5882
|
try {
|
|
2760
5883
|
const universalTxResp = yield this.pushClient.getUniversalTxById(idHex);
|
|
2761
5884
|
universalTxObj = universalTxResp === null || universalTxResp === void 0 ? void 0 : universalTxResp.universalTx;
|
|
2762
|
-
if (universalTxObj)
|
|
5885
|
+
if (universalTxObj) {
|
|
2763
5886
|
break;
|
|
5887
|
+
}
|
|
2764
5888
|
}
|
|
2765
5889
|
catch (error) {
|
|
2766
5890
|
// ignore and retry
|
|
2767
|
-
// console.log(error);
|
|
2768
5891
|
}
|
|
2769
5892
|
// Linear delay for first N attempts, then exponential backoff
|
|
2770
5893
|
let delay;
|
|
@@ -2780,7 +5903,7 @@ class Orchestrator {
|
|
|
2780
5903
|
}
|
|
2781
5904
|
return universalTxObj;
|
|
2782
5905
|
}
|
|
2783
|
-
catch (
|
|
5906
|
+
catch (err) {
|
|
2784
5907
|
return undefined;
|
|
2785
5908
|
}
|
|
2786
5909
|
});
|
|
@@ -3018,4 +6141,8 @@ class Orchestrator {
|
|
|
3018
6141
|
}
|
|
3019
6142
|
}
|
|
3020
6143
|
exports.Orchestrator = Orchestrator;
|
|
6144
|
+
// Outbound sync configuration constants
|
|
6145
|
+
Orchestrator.OUTBOUND_INITIAL_WAIT_MS = 30000; // 30s
|
|
6146
|
+
Orchestrator.OUTBOUND_POLL_INTERVAL_MS = 5000; // 5s
|
|
6147
|
+
Orchestrator.OUTBOUND_MAX_TIMEOUT_MS = 180000; // 180s (3 min)
|
|
3021
6148
|
//# sourceMappingURL=orchestrator.js.map
|