@metamask-previews/transaction-pay-controller 30.1.0-preview-0bedd99c0 → 30.1.0-preview-5e24e4d22
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 +8 -0
- package/dist/strategy/server/deposit-step.d.ts +18 -0
- package/dist/strategy/server/deposit-step.d.ts.map +1 -0
- package/dist/strategy/server/deposit-step.js +37 -0
- package/dist/strategy/server/deposit-step.js.map +1 -0
- package/dist/strategy/server/server-quotes.d.ts.map +1 -1
- package/dist/strategy/server/server-quotes.js +163 -51
- package/dist/strategy/server/server-quotes.js.map +1 -1
- package/dist/strategy/server/server-submit.d.ts.map +1 -1
- package/dist/strategy/server/server-submit.js +118 -16
- package/dist/strategy/server/server-submit.js.map +1 -1
- package/dist/strategy/server/server-support.d.ts +1 -3
- package/dist/strategy/server/server-support.d.ts.map +1 -1
- package/dist/strategy/server/server-support.js +0 -5
- package/dist/strategy/server/server-support.js.map +1 -1
- package/dist/strategy/server/types.d.ts +7 -0
- package/dist/strategy/server/types.d.ts.map +1 -1
- package/dist/strategy/server/types.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Support Predict withdraw in `ServerStrategy` ([#10702](https://github.com/MetaMask/core/pull/10702))
|
|
13
|
+
- Polymarket deposit wallet withdrawals unwrap directly to a provider deposit address, then sweep any leftover or refunded USDC.e back into the deposit wallet.
|
|
14
|
+
- Polymarket deposit wallet withdrawals have no source network fee, as the Polymarket relayer pays origin gas.
|
|
15
|
+
- Non-atomic post-quote payment overrides, such as withdrawing to a Money Account, settle on the override recipient and run the override calls as a second leg.
|
|
16
|
+
- Post-quote quotes honour `refundTo`, include the original transaction in gas estimation, and reserve the gas fee token cost from the source amount.
|
|
17
|
+
|
|
10
18
|
### Changed
|
|
11
19
|
|
|
12
20
|
- Bump `@metamask/transaction-controller` from `^72.2.0` to `^72.2.1` ([#10817](https://github.com/MetaMask/core/pull/10817))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Hex } from '@metamask/utils';
|
|
2
|
+
import type { ServerQuote } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether a server quote routes through a deposit step, i.e. the user funds the
|
|
5
|
+
* request by transferring to the provider rather than calling a swap contract.
|
|
6
|
+
*
|
|
7
|
+
* @param steps - Server quote steps.
|
|
8
|
+
* @returns True when the quote has a deposit step.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hasServerDepositStep(steps: ServerQuote['steps']): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Get the calldata of the server deposit transaction step.
|
|
13
|
+
*
|
|
14
|
+
* @param quote - Server quote.
|
|
15
|
+
* @returns The deposit calldata, or `undefined` when there is none.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getServerDepositData(quote: ServerQuote): Hex | undefined;
|
|
18
|
+
//# sourceMappingURL=deposit-step.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deposit-step.d.ts","sourceRoot":"","sources":["../../../src/strategy/server/deposit-step.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAG3C,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,YAAY,CAAC;AAOrE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,OAAO,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,GAAG,SAAS,CAExE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BigNumber } from 'bignumber.js';
|
|
2
|
+
const TOKEN_TRANSFER_FOUR_BYTE = '0xa9059cbb';
|
|
3
|
+
// `0x` + 4-byte selector + two 32-byte arguments (recipient, amount).
|
|
4
|
+
const TOKEN_TRANSFER_DATA_LENGTH = 2 + 8 + 64 * 2;
|
|
5
|
+
/**
|
|
6
|
+
* Whether a server quote routes through a deposit step, i.e. the user funds the
|
|
7
|
+
* request by transferring to the provider rather than calling a swap contract.
|
|
8
|
+
*
|
|
9
|
+
* @param steps - Server quote steps.
|
|
10
|
+
* @returns True when the quote has a deposit step.
|
|
11
|
+
*/
|
|
12
|
+
export function hasServerDepositStep(steps) {
|
|
13
|
+
return steps.some(isDepositStep);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get the calldata of the server deposit transaction step.
|
|
17
|
+
*
|
|
18
|
+
* @param quote - Server quote.
|
|
19
|
+
* @returns The deposit calldata, or `undefined` when there is none.
|
|
20
|
+
*/
|
|
21
|
+
export function getServerDepositData(quote) {
|
|
22
|
+
return quote.steps.find(isDepositStep)?.data;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Whether a step is a plain ERC-20 `transfer` with no extra calldata and no
|
|
26
|
+
* native value, which is how a provider deposit address is funded.
|
|
27
|
+
*
|
|
28
|
+
* @param step - Server quote step.
|
|
29
|
+
* @returns True when the step is a plain token transfer.
|
|
30
|
+
*/
|
|
31
|
+
function isDepositStep(step) {
|
|
32
|
+
return (step.type === 'transaction' &&
|
|
33
|
+
step.data.length === TOKEN_TRANSFER_DATA_LENGTH &&
|
|
34
|
+
step.data.toLowerCase().startsWith(TOKEN_TRANSFER_FOUR_BYTE) &&
|
|
35
|
+
new BigNumber(step.value || '0').isZero());
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=deposit-step.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deposit-step.js","sourceRoot":"","sources":["../../../src/strategy/server/deposit-step.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAIzC,MAAM,wBAAwB,GAAG,YAAY,CAAC;AAE9C,sEAAsE;AACtE,MAAM,0BAA0B,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAA2B;IAC9D,OAAO,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAkB;IACrD,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC;AAC/C,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CACpB,IAAkC;IAElC,OAAO,CACL,IAAI,CAAC,IAAI,KAAK,aAAa;QAC3B,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,0BAA0B;QAC/C,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,wBAAwB,CAAC;QAC5D,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,CAC1C,CAAC;AACJ,CAAC","sourcesContent":["import type { Hex } from '@metamask/utils';\nimport { BigNumber } from 'bignumber.js';\n\nimport type { ServerQuote, ServerTransactionStep } from './types.js';\n\nconst TOKEN_TRANSFER_FOUR_BYTE = '0xa9059cbb';\n\n// `0x` + 4-byte selector + two 32-byte arguments (recipient, amount).\nconst TOKEN_TRANSFER_DATA_LENGTH = 2 + 8 + 64 * 2;\n\n/**\n * Whether a server quote routes through a deposit step, i.e. the user funds the\n * request by transferring to the provider rather than calling a swap contract.\n *\n * @param steps - Server quote steps.\n * @returns True when the quote has a deposit step.\n */\nexport function hasServerDepositStep(steps: ServerQuote['steps']): boolean {\n return steps.some(isDepositStep);\n}\n\n/**\n * Get the calldata of the server deposit transaction step.\n *\n * @param quote - Server quote.\n * @returns The deposit calldata, or `undefined` when there is none.\n */\nexport function getServerDepositData(quote: ServerQuote): Hex | undefined {\n return quote.steps.find(isDepositStep)?.data;\n}\n\n/**\n * Whether a step is a plain ERC-20 `transfer` with no extra calldata and no\n * native value, which is how a provider deposit address is funded.\n *\n * @param step - Server quote step.\n * @returns True when the step is a plain token transfer.\n */\nfunction isDepositStep(\n step: ServerQuote['steps'][number],\n): step is ServerTransactionStep {\n return (\n step.type === 'transaction' &&\n step.data.length === TOKEN_TRANSFER_DATA_LENGTH &&\n step.data.toLowerCase().startsWith(TOKEN_TRANSFER_FOUR_BYTE) &&\n new BigNumber(step.value || '0').isZero()\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-quotes.d.ts","sourceRoot":"","sources":["../../../src/strategy/server/server-quotes.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,2BAA2B,EAI3B,mBAAmB,EACpB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"server-quotes.d.ts","sourceRoot":"","sources":["../../../src/strategy/server/server-quotes.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,2BAA2B,EAI3B,mBAAmB,EACpB,MAAM,gBAAgB,CAAC;AAuBxB,OAAO,KAAK,EACV,WAAW,EAKZ,MAAM,YAAY,CAAC;AA6CpB;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE,CAAC,CAY7C"}
|
|
@@ -5,12 +5,16 @@ import { BigNumber } from 'bignumber.js';
|
|
|
5
5
|
import { CHAIN_ID_HYPERCORE, PaymentOverride, TransactionPayStrategy, } from '../../constants.js';
|
|
6
6
|
import { projectLogger } from '../../logger.js';
|
|
7
7
|
import { getFiatValueFromUsd } from '../../utils/amounts.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getSlippage, isEIP7702Chain } from '../../utils/feature-flags.js';
|
|
9
9
|
import { GasPaymentMode, resolveGasPayment, resolveGasStationCost, } from '../../utils/gas-payment.js';
|
|
10
10
|
import { calculateGasCost, getGasFee } from '../../utils/gas.js';
|
|
11
|
-
import {
|
|
11
|
+
import { getPolymarketDepositWalletOverrides, getPredictWithdrawFeeTokenAccount, getPredictWithdrawSafeAddress, } from '../../utils/polymarket/withdraw.js';
|
|
12
|
+
import { reservePostQuoteGas } from '../../utils/post-quote.js';
|
|
13
|
+
import { estimateSourceGas } from '../../utils/quote-gas.js';
|
|
14
|
+
import { resolveNonAtomicRecipient } from '../../utils/second-leg.js';
|
|
12
15
|
import { getTokenFiatRate } from '../../utils/token.js';
|
|
13
16
|
import { getQuotePricing, TradeType } from '../../utils/trade-type.js';
|
|
17
|
+
import { hasServerDepositStep } from './deposit-step.js';
|
|
14
18
|
import { normalizeServerPerpsRequest } from './perps.js';
|
|
15
19
|
import { fetchServerQuote } from './server-api.js';
|
|
16
20
|
const log = createModuleLogger(projectLogger, 'server-quotes');
|
|
@@ -37,31 +41,68 @@ export async function getServerQuotes(request) {
|
|
|
37
41
|
return quotes.flat();
|
|
38
42
|
}
|
|
39
43
|
async function getQuotesForRequest(quoteRequest, fullRequest) {
|
|
40
|
-
const { accountSupports7702, messenger, signal, transaction } = fullRequest;
|
|
41
44
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
const normalized = await Promise.all(fulfilledResults.map((result) => normalizeQuote(result, settledRequest, messenger, body.tradeType === TradeType.ExactInput, transaction, accountSupports7702, isSecondLegRequired(body, quoteRequest, transaction, result.quote))));
|
|
54
|
-
log('Normalized quotes', normalized);
|
|
55
|
-
return normalized;
|
|
45
|
+
const quotes = await fetchQuotes(quoteRequest, fullRequest);
|
|
46
|
+
if (!quoteRequest.isPostQuote) {
|
|
47
|
+
return quotes;
|
|
48
|
+
}
|
|
49
|
+
// Post-quote flows commit the whole source balance, so any gas paid in the
|
|
50
|
+
// source token has to be reserved by re-quoting the same provider for less.
|
|
51
|
+
return await Promise.all(quotes.map((quote) => reservePostQuoteGas({
|
|
52
|
+
quote,
|
|
53
|
+
requote: (adjustedRequest) => requoteProvider(adjustedRequest, fullRequest, quote.original),
|
|
54
|
+
})));
|
|
56
55
|
}
|
|
57
56
|
catch (error) {
|
|
58
57
|
log('Error fetching quotes', { error });
|
|
59
58
|
return [];
|
|
60
59
|
}
|
|
61
60
|
}
|
|
61
|
+
async function requoteProvider(quoteRequest, fullRequest, { provider }) {
|
|
62
|
+
const [quote] = await fetchQuotes(quoteRequest, fullRequest, provider);
|
|
63
|
+
if (!quote) {
|
|
64
|
+
throw new Error(`No re-quote from provider: ${provider}`);
|
|
65
|
+
}
|
|
66
|
+
return quote;
|
|
67
|
+
}
|
|
68
|
+
async function fetchQuotes(quoteRequest, fullRequest, provider) {
|
|
69
|
+
const { accountSupports7702, messenger, signal, transaction } = fullRequest;
|
|
70
|
+
const settledRequest = await resolveSettledRequest(quoteRequest, transaction, messenger);
|
|
71
|
+
const body = await buildServerQuoteRequest(settledRequest, transaction, messenger, accountSupports7702);
|
|
72
|
+
if (provider) {
|
|
73
|
+
body.providers = [provider];
|
|
74
|
+
}
|
|
75
|
+
log('Request body', body);
|
|
76
|
+
const response = await fetchServerQuote(messenger, body, signal);
|
|
77
|
+
log('Raw quote response', response);
|
|
78
|
+
const fulfilledResults = response.results.filter((result) => isFulfilledResult(result) &&
|
|
79
|
+
(!provider || result.provider === provider) &&
|
|
80
|
+
isUsableDepositAddressQuote(body, result));
|
|
81
|
+
const normalized = await Promise.all(fulfilledResults.map((result) => normalizeQuote(result, settledRequest, messenger, body, transaction, accountSupports7702, isSecondLegRequired(body, settledRequest, transaction, result.quote))));
|
|
82
|
+
log('Normalized quotes', normalized);
|
|
83
|
+
return normalized;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Carry the account the funds settle on as the request recipient.
|
|
87
|
+
*
|
|
88
|
+
* The second leg runs from `request.recipient`, which has to match where the
|
|
89
|
+
* funds actually landed. Non-atomic flows settle on the account submitting the
|
|
90
|
+
* second leg, such as the Money Account of a Predict withdraw. Otherwise the
|
|
91
|
+
* quote settles on the executing account rather than the payer.
|
|
92
|
+
*
|
|
93
|
+
* @param quoteRequest - Quote request.
|
|
94
|
+
* @param transaction - Original transaction meta.
|
|
95
|
+
* @param messenger - Controller messenger.
|
|
96
|
+
* @returns The request with the settlement recipient applied.
|
|
97
|
+
*/
|
|
98
|
+
async function resolveSettledRequest(quoteRequest, transaction, messenger) {
|
|
99
|
+
const recipient = (await resolveNonAtomicRecipient(transaction, quoteRequest, messenger)) ??
|
|
100
|
+
transaction.txParams.from;
|
|
101
|
+
return { ...quoteRequest, recipient };
|
|
102
|
+
}
|
|
62
103
|
async function buildServerQuoteRequest(quoteRequest, transaction, messenger, accountSupports7702) {
|
|
63
104
|
const normalizedRequest = normalizeServerPerpsRequest(quoteRequest, transaction);
|
|
64
|
-
const { atomic, from, isMaxAmount, isPostQuote, paymentOverride, sourceChainId, sourceTokenAddress, sourceTokenAmount, targetAmountMinimum, targetChainId, targetTokenAddress, } = normalizedRequest;
|
|
105
|
+
const { atomic, from, isMaxAmount, isPolymarketDepositWallet, isPostQuote, paymentOverride, refundTo, sourceChainId, sourceTokenAddress, sourceTokenAmount, targetAmountMinimum, targetChainId, targetTokenAddress, } = normalizedRequest;
|
|
65
106
|
const singleData = getSingleTransactionData(transaction);
|
|
66
107
|
const isHypercore = targetChainId === CHAIN_ID_HYPERCORE;
|
|
67
108
|
const isTokenTransfer = !isHypercore && Boolean(singleData?.startsWith(TOKEN_TRANSFER_FOUR_BYTE));
|
|
@@ -70,9 +111,12 @@ async function buildServerQuoteRequest(quoteRequest, transaction, messenger, acc
|
|
|
70
111
|
const executionAccount = transaction.txParams.from;
|
|
71
112
|
const recipient = isTokenTransfer && singleData
|
|
72
113
|
? decodeTransferRecipient(singleData)
|
|
73
|
-
: executionAccount;
|
|
114
|
+
: (normalizedRequest.recipient ?? executionAccount);
|
|
74
115
|
const isHypercoreSource = sourceChainId === CHAIN_ID_HYPERCORE;
|
|
116
|
+
// Deposit wallet withdraws are broadcast by the Polymarket relayer rather
|
|
117
|
+
// than as a gasless EIP-7702 batch, so the provider must not quote gasless.
|
|
75
118
|
const supportsGasless = !isHypercoreSource &&
|
|
119
|
+
!isPolymarketDepositWallet &&
|
|
76
120
|
accountSupports7702 &&
|
|
77
121
|
isEIP7702Chain(messenger, sourceChainId);
|
|
78
122
|
const body = {
|
|
@@ -83,6 +127,9 @@ async function buildServerQuoteRequest(quoteRequest, transaction, messenger, acc
|
|
|
83
127
|
slippage: Math.round(getSlippage(messenger, sourceChainId, sourceTokenAddress) * 10000),
|
|
84
128
|
supportsGasless,
|
|
85
129
|
};
|
|
130
|
+
if (isPolymarketDepositWallet) {
|
|
131
|
+
await applyDepositWalletOverrides(body, from, messenger);
|
|
132
|
+
}
|
|
86
133
|
const hasNoData = singleData === undefined || singleData === '0x';
|
|
87
134
|
const skipDelegation = hasNoData ||
|
|
88
135
|
isTokenTransfer ||
|
|
@@ -92,8 +139,13 @@ async function buildServerQuoteRequest(quoteRequest, transaction, messenger, acc
|
|
|
92
139
|
// they must not be embedded in the quote.
|
|
93
140
|
atomic === false ||
|
|
94
141
|
(isPostQuote ?? false) ||
|
|
95
|
-
(isMaxAmount ?? false)
|
|
96
|
-
|
|
142
|
+
(isMaxAmount ?? false) ||
|
|
143
|
+
// The deposit wallet funds the quote with a plain token transfer, so there
|
|
144
|
+
// are no calls to embed.
|
|
145
|
+
(isPolymarketDepositWallet ?? false);
|
|
146
|
+
if (isPostQuote &&
|
|
147
|
+
atomic !== false &&
|
|
148
|
+
paymentOverride === PaymentOverride.MoneyAccount) {
|
|
97
149
|
await processMoneyAccountPostQuote(transaction, normalizedRequest, body, messenger);
|
|
98
150
|
}
|
|
99
151
|
else if (!skipDelegation) {
|
|
@@ -127,6 +179,12 @@ async function buildServerQuoteRequest(quoteRequest, transaction, messenger, acc
|
|
|
127
179
|
body.authorizationList = normalizeAuthorizationList(delegation.authorizationList);
|
|
128
180
|
}
|
|
129
181
|
}
|
|
182
|
+
else if (isPostQuote || atomic === false) {
|
|
183
|
+
// Honour the caller's refund address so a failed quote refunds the account
|
|
184
|
+
// the funds came from, such as the Predict Safe, rather than the payer. A
|
|
185
|
+
// deposit wallet refund address is kept, as the sweep reads refunds there.
|
|
186
|
+
body.refundTo ??= refundTo;
|
|
187
|
+
}
|
|
130
188
|
const pricing = getQuotePricing({
|
|
131
189
|
hasCalls: Boolean(body.calls?.length),
|
|
132
190
|
sourceTokenAmount,
|
|
@@ -170,6 +228,24 @@ function canDeferCalls({ accountSupports7702, messenger, targetChainId, transact
|
|
|
170
228
|
transaction.chainId === targetChainId &&
|
|
171
229
|
isEIP7702Chain(messenger, targetChainId));
|
|
172
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Quote a deposit wallet Predict withdraw from the deposit wallet itself.
|
|
233
|
+
*
|
|
234
|
+
* The deposit wallet funds the provider's deposit address with the USDC.e
|
|
235
|
+
* unwrapped from its pUSD, and is also the refund address so the sweep can wrap
|
|
236
|
+
* any refund back into pUSD.
|
|
237
|
+
*
|
|
238
|
+
* @param body - Quote request body to update.
|
|
239
|
+
* @param from - The user EOA that owns the deposit wallet.
|
|
240
|
+
* @param messenger - Controller messenger.
|
|
241
|
+
*/
|
|
242
|
+
async function applyDepositWalletOverrides(body, from, messenger) {
|
|
243
|
+
const { depositWallet, sourceTokenAddress } = await getPolymarketDepositWalletOverrides(from, messenger);
|
|
244
|
+
body.source = { ...body.source, token: sourceTokenAddress };
|
|
245
|
+
body.sender = depositWallet;
|
|
246
|
+
body.refundTo = depositWallet;
|
|
247
|
+
body.useDepositAddress = true;
|
|
248
|
+
}
|
|
173
249
|
function normalizeAuthorizationList(authorizationList) {
|
|
174
250
|
return authorizationList.map((entry) => ({
|
|
175
251
|
address: entry.address,
|
|
@@ -230,6 +306,8 @@ function shouldRequestQuote(quoteRequest) {
|
|
|
230
306
|
*
|
|
231
307
|
* A second leg is needed either because we declined to embed the calls, or
|
|
232
308
|
* because the provider that won the quote told us it will not execute them.
|
|
309
|
+
* Non-atomic post-quote flows always need one: their calls come from the
|
|
310
|
+
* payment override rather than the transaction, and run once funds settle.
|
|
233
311
|
*
|
|
234
312
|
* @param body - The built server quote request.
|
|
235
313
|
* @param quoteRequest - The originating quote request.
|
|
@@ -243,6 +321,19 @@ function isSecondLegRequired(body, quoteRequest, transaction, quote) {
|
|
|
243
321
|
if (body.calls?.length && !quote.callsDeferred) {
|
|
244
322
|
return false;
|
|
245
323
|
}
|
|
324
|
+
// HyperCore settles off-chain, so there is no target-chain transfer to read
|
|
325
|
+
// a settled amount from. Checks the built body rather than the original
|
|
326
|
+
// request, since perps deposits are only rewritten to HyperCore there.
|
|
327
|
+
if (body.target.chainId === HYPERCORE_CHAIN_ID_NUMBER ||
|
|
328
|
+
body.source.chainId === HYPERCORE_CHAIN_ID_NUMBER) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
// Post-quote flows submit the original transaction alongside the quote, so
|
|
332
|
+
// the second leg never re-runs it. Only non-atomic flows have override calls
|
|
333
|
+
// left to run once the funds settle.
|
|
334
|
+
if (quoteRequest.isPostQuote) {
|
|
335
|
+
return quoteRequest.atomic === false;
|
|
336
|
+
}
|
|
246
337
|
// No calls to run. Plain funding transfers and empty calldata are already
|
|
247
338
|
// satisfied by the quote delivering the target token to the recipient.
|
|
248
339
|
if (!transaction.nestedTransactions?.length) {
|
|
@@ -255,23 +346,12 @@ function isSecondLegRequired(body, quoteRequest, transaction, quote) {
|
|
|
255
346
|
if (singleData.startsWith(TOKEN_TRANSFER_FOUR_BYTE)) {
|
|
256
347
|
return false;
|
|
257
348
|
}
|
|
258
|
-
// HyperCore settles off-chain, so there is no target-chain transfer to read
|
|
259
|
-
// a settled amount from. Checks the built body rather than the original
|
|
260
|
-
// request, since perps deposits are only rewritten to HyperCore there.
|
|
261
|
-
if (body.target.chainId === HYPERCORE_CHAIN_ID_NUMBER ||
|
|
262
|
-
body.source.chainId === HYPERCORE_CHAIN_ID_NUMBER) {
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
// Post-quote flows submit the original transaction separately, so running it
|
|
266
|
-
// again as a second leg would double-execute it.
|
|
267
|
-
if (quoteRequest.isPostQuote) {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
349
|
return true;
|
|
271
350
|
}
|
|
272
|
-
async function normalizeQuote(result, quoteRequest, messenger,
|
|
351
|
+
async function normalizeQuote(result, quoteRequest, messenger, body, transaction, accountSupports7702, requiresSecondLeg) {
|
|
273
352
|
const { quote } = result;
|
|
274
353
|
const { gasless } = quote;
|
|
354
|
+
const isInputBased = body.tradeType === TradeType.ExactInput;
|
|
275
355
|
const transactionSteps = quote.steps.filter(isTransactionStep);
|
|
276
356
|
const isSignatureOnly = transactionSteps.length === 0;
|
|
277
357
|
const sourceNetwork = await calculateSourceNetworkCost({
|
|
@@ -279,6 +359,7 @@ async function normalizeQuote(result, quoteRequest, messenger, isInputBased, tra
|
|
|
279
359
|
gasless: gasless || isSignatureOnly,
|
|
280
360
|
messenger,
|
|
281
361
|
quoteRequest,
|
|
362
|
+
sender: body.sender,
|
|
282
363
|
steps: transactionSteps,
|
|
283
364
|
transaction,
|
|
284
365
|
});
|
|
@@ -352,7 +433,7 @@ async function normalizeQuote(result, quoteRequest, messenger, isInputBased, tra
|
|
|
352
433
|
},
|
|
353
434
|
};
|
|
354
435
|
}
|
|
355
|
-
async function calculateSourceNetworkCost({ accountSupports7702, gasless, messenger, quoteRequest, steps, transaction, }) {
|
|
436
|
+
async function calculateSourceNetworkCost({ accountSupports7702, gasless, messenger, quoteRequest, sender, steps, transaction, }) {
|
|
356
437
|
const noFees = {
|
|
357
438
|
estimate: ZERO_AMOUNT,
|
|
358
439
|
gasLimits: [],
|
|
@@ -361,9 +442,11 @@ async function calculateSourceNetworkCost({ accountSupports7702, gasless, messen
|
|
|
361
442
|
maxFeePerGas: undefined,
|
|
362
443
|
maxPriorityFeePerGas: undefined,
|
|
363
444
|
};
|
|
364
|
-
const { from, sourceChainId, sourceTokenAddress } = quoteRequest;
|
|
445
|
+
const { from, isPolymarketDepositWallet, sourceChainId, sourceTokenAddress } = quoteRequest;
|
|
446
|
+
// A deposit wallet withdraw is broadcast by the Polymarket relayer, which
|
|
447
|
+
// pays the gas, so the user only signs.
|
|
365
448
|
const gasPayment = resolveGasPayment({
|
|
366
|
-
isDelegated: gasless,
|
|
449
|
+
isDelegated: gasless || Boolean(isPolymarketDepositWallet),
|
|
367
450
|
sourceTokenAddress,
|
|
368
451
|
sponsorship: {
|
|
369
452
|
accountSupports7702,
|
|
@@ -372,7 +455,10 @@ async function calculateSourceNetworkCost({ accountSupports7702, gasless, messen
|
|
|
372
455
|
},
|
|
373
456
|
});
|
|
374
457
|
if (gasPayment.mode === GasPaymentMode.Delegation) {
|
|
375
|
-
log('Zeroing source network fees
|
|
458
|
+
log('Zeroing source network fees as the user does not pay origin gas', {
|
|
459
|
+
gasless,
|
|
460
|
+
isPolymarketDepositWallet,
|
|
461
|
+
});
|
|
376
462
|
return noFees;
|
|
377
463
|
}
|
|
378
464
|
if (gasPayment.mode === GasPaymentMode.Sponsored) {
|
|
@@ -389,34 +475,38 @@ async function calculateSourceNetworkCost({ accountSupports7702, gasless, messen
|
|
|
389
475
|
: { maxFeePerGas: undefined, maxPriorityFeePerGas: undefined };
|
|
390
476
|
const maxFeePerGas = firstStep.maxFeePerGas ?? gasFeeEstimate.maxFeePerGas;
|
|
391
477
|
const maxPriorityFeePerGas = firstStep.maxPriorityFeePerGas ?? gasFeeEstimate.maxPriorityFeePerGas;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
478
|
+
// Legacy Safe Predict withdraws hold the source token in the Safe, so deposit
|
|
479
|
+
// routes are estimated from the Safe. Swap routes keep the sender, as DEX
|
|
480
|
+
// aggregators reject contract callers.
|
|
481
|
+
const fromOverride = getPredictWithdrawSafeAddress(quoteRequest, transaction, hasServerDepositStep(steps));
|
|
482
|
+
const { gasLimits, is7702, totalGasEstimate, totalGasLimit } = await estimateSourceGas({
|
|
483
|
+
fromOverride,
|
|
396
484
|
messenger,
|
|
397
|
-
|
|
485
|
+
request: quoteRequest,
|
|
486
|
+
transaction,
|
|
487
|
+
transactions: steps.map((step) => stepToGasTransaction(step, sender)),
|
|
398
488
|
});
|
|
399
|
-
const { is7702 } = gasResult;
|
|
400
|
-
const gasLimits = is7702
|
|
401
|
-
? [gasResult.gasLimits[0].max]
|
|
402
|
-
: gasResult.gasLimits.map((gasLimit) => gasLimit.max);
|
|
403
489
|
const estimate = calculateGasCost({
|
|
404
490
|
chainId: chainIdHex,
|
|
405
|
-
gas:
|
|
491
|
+
gas: totalGasEstimate,
|
|
406
492
|
maxFeePerGas: maxFeePerGas ?? '0',
|
|
407
493
|
maxPriorityFeePerGas: maxPriorityFeePerGas ?? '0',
|
|
408
494
|
messenger,
|
|
409
495
|
});
|
|
410
496
|
const max = calculateGasCost({
|
|
411
497
|
chainId: chainIdHex,
|
|
412
|
-
gas:
|
|
498
|
+
gas: totalGasLimit,
|
|
413
499
|
isMax: true,
|
|
414
500
|
maxFeePerGas: maxFeePerGas ?? '0',
|
|
415
501
|
maxPriorityFeePerGas: maxPriorityFeePerGas ?? '0',
|
|
416
502
|
messenger,
|
|
417
503
|
});
|
|
418
504
|
const fees = { maxFeePerGas, maxPriorityFeePerGas };
|
|
505
|
+
// Every Predict withdraw holds the source token in the Safe, so the fee token
|
|
506
|
+
// lookup must use it even on swap routes.
|
|
507
|
+
const feeTokenAccount = getPredictWithdrawFeeTokenAccount(quoteRequest, transaction);
|
|
419
508
|
const gasStationCost = await resolveGasStationCost({
|
|
509
|
+
feeTokenAccount,
|
|
420
510
|
firstStepData: {
|
|
421
511
|
data: firstStep.data,
|
|
422
512
|
to: firstStep.to,
|
|
@@ -429,13 +519,16 @@ async function calculateSourceNetworkCost({ accountSupports7702, gasless, messen
|
|
|
429
519
|
sourceChainId,
|
|
430
520
|
sourceTokenAddress,
|
|
431
521
|
},
|
|
432
|
-
totalGasEstimate
|
|
433
|
-
totalItemCount:
|
|
522
|
+
totalGasEstimate,
|
|
523
|
+
totalItemCount: feeTokenAccount
|
|
524
|
+
? steps.length + 1
|
|
525
|
+
: Math.max(steps.length, gasLimits.length),
|
|
434
526
|
});
|
|
435
527
|
if (!gasStationCost.amount) {
|
|
436
528
|
return { estimate, gasLimits, is7702, max, ...fees };
|
|
437
529
|
}
|
|
438
530
|
log('Using gas fee token for source network', {
|
|
531
|
+
feeTokenAccount,
|
|
439
532
|
gasFeeTokenCost: gasStationCost.amount,
|
|
440
533
|
});
|
|
441
534
|
return {
|
|
@@ -469,6 +562,25 @@ function isFulfilledResult(result) {
|
|
|
469
562
|
result.quote.input !== undefined &&
|
|
470
563
|
result.quote.output !== undefined);
|
|
471
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Whether a quote can be funded as requested. A deposit-address request is only
|
|
567
|
+
* usable when the quote funds the provider with a plain token transfer, since
|
|
568
|
+
* that transfer is what gets replaced by an unwrap straight to the deposit
|
|
569
|
+
* address.
|
|
570
|
+
*
|
|
571
|
+
* @param body - The built server quote request.
|
|
572
|
+
* @param result - Fulfilled provider result.
|
|
573
|
+
* @returns True when the quote is usable.
|
|
574
|
+
*/
|
|
575
|
+
function isUsableDepositAddressQuote(body, result) {
|
|
576
|
+
if (!body.useDepositAddress || hasServerDepositStep(result.quote.steps)) {
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
log('Ignoring deposit address quote without a plain token transfer', {
|
|
580
|
+
provider: result.provider,
|
|
581
|
+
});
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
472
584
|
function decodeTransferRecipient(data) {
|
|
473
585
|
return TRANSFER_INTERFACE.decodeFunctionData('transfer', data).to.toLowerCase();
|
|
474
586
|
}
|