Skip to content

Commit 284e24d

Browse files
authored
sendCalls in executeQuote if the wallet supports it (#229)
* sendCalls if the wallet supports it * optional param, rename, approve input, simulate
1 parent 050a2e4 commit 284e24d

File tree

7 files changed

+857
-453
lines changed

7 files changed

+857
-453
lines changed

.changeset/thirty-snails-camp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@across-protocol/app-sdk": patch
3+
---
4+
5+
Added atomic sendCalls with fallback to `executeQuote`

apps/example/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"tailwind-merge": "^2.6.0",
3737
"tailwindcss-animate": "^1.0.7",
3838
"usehooks-ts": "^3.1.0",
39-
"viem": "^2.20.1",
39+
"viem": "^2.31.2",
4040
"wagmi": "^2.12.25"
4141
},
4242
"devDependencies": {
@@ -53,4 +53,4 @@
5353
"tsx": "^4.19.0",
5454
"typescript": "^5"
5555
}
56-
}
56+
}

packages/sdk/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@
5353
"typedoc": "^0.26.7",
5454
"typedoc-plugin-markdown": "^4.4.1",
5555
"typescript": "^5.3.3",
56-
"viem": "^2.20.1",
56+
"viem": "2.31.2",
5757
"vitest": "^2.0.5",
5858
"zod": "^3.24.2"
5959
},
6060
"publishConfig": {
6161
"access": "public"
6262
},
6363
"peerDependencies": {
64-
"viem": "^2.20.1"
64+
"viem": "^2.31.2"
6565
}
66-
}
66+
}

packages/sdk/src/actions/executeQuote.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
} from "../types/index.js";
1919
import { parseFillLogs, waitForFillTx } from "./waitForFillTx.js";
2020
import { parseDepositLogs } from "./getDepositFromLogs.js";
21+
import { prepareAtomicApproveDepositTx } from "./prepareAtomicApproveDepositTx.js";
22+
import { waitForAtomicTx } from "./waitForAtomicTx.js";
2123

2224
export type ExecutionProgress = TransactionProgress;
2325

@@ -161,6 +163,10 @@ export type ExecuteQuoteParams = {
161163
* The logger to use.
162164
*/
163165
logger?: LoggerT;
166+
/**
167+
* Whether to use atomic transactions if supported by the wallet.
168+
*/
169+
atomicIfSupported?: boolean;
164170
};
165171

166172
/**
@@ -194,7 +200,9 @@ export type ExecuteQuoteResponseParams = {
194200
* @returns The deposit ID and receipts for the deposit and fill transactions. See {@link ExecuteQuoteResponseParams}.
195201
* @public
196202
*/
197-
export async function executeQuote(params: ExecuteQuoteParams): Promise<ExecuteQuoteResponseParams> {
203+
export async function executeQuote(
204+
params: ExecuteQuoteParams,
205+
): Promise<ExecuteQuoteResponseParams> {
198206
const {
199207
integratorId,
200208
deposit,
@@ -207,6 +215,7 @@ export async function executeQuote(params: ExecuteQuoteParams): Promise<ExecuteQ
207215
forceOriginChain,
208216
onProgress,
209217
logger,
218+
atomicIfSupported = false,
210219
} = params;
211220

212221
const onProgressHandler =
@@ -281,6 +290,111 @@ export async function executeQuote(params: ExecuteQuoteParams): Promise<ExecuteQ
281290

282291
onProgressHandler(currentTransactionProgress);
283292

293+
if (atomicIfSupported) {
294+
logger?.debug("Checking if wallet supports atomic transactions");
295+
try {
296+
const capabilities = await walletClient.getCapabilities({
297+
account,
298+
chainId: deposit.originChainId,
299+
});
300+
301+
if (
302+
capabilities?.atomic?.status === "supported" ||
303+
capabilities?.atomic?.status === "ready"
304+
) {
305+
logger?.debug(
306+
"Wallet supports atomic sendCalls, triggering atomic flow",
307+
);
308+
// Simulate both approval and deposit calls
309+
const { calls } = await prepareAtomicApproveDepositTx({
310+
walletClient,
311+
publicClient: originClient,
312+
deposit,
313+
approvalAmount: BigInt(inputAmount),
314+
integratorId,
315+
logger,
316+
});
317+
318+
const { id: callId } = await walletClient.sendCalls({
319+
account,
320+
calls,
321+
forceAtomic: true,
322+
});
323+
324+
logger?.debug(`Atomic call ID: ${callId}`);
325+
326+
const destinationBlock = await destinationClient.getBlockNumber();
327+
328+
const { depositId, depositTxReceipt } = await waitForAtomicTx({
329+
callId,
330+
originChainId: deposit.originChainId,
331+
walletClient,
332+
});
333+
334+
const depositLog = parseDepositLogs(depositTxReceipt.logs);
335+
336+
const depositSuccessProgress: TransactionProgress = {
337+
step: "deposit",
338+
status: "txSuccess",
339+
txReceipt: depositTxReceipt,
340+
depositId,
341+
depositLog,
342+
meta: { deposit },
343+
};
344+
onProgressHandler(depositSuccessProgress);
345+
346+
// After successful deposit, wait for fill
347+
const fillMeta: FillMeta = {
348+
depositId,
349+
deposit,
350+
};
351+
const fillPendingProgress: TransactionProgress = {
352+
step: "fill",
353+
status: "txPending",
354+
meta: fillMeta,
355+
};
356+
onProgressHandler(fillPendingProgress);
357+
358+
const { fillTxReceipt, fillTxTimestamp, actionSuccess } =
359+
await waitForFillTx({
360+
deposit,
361+
depositId,
362+
depositTxHash: depositTxReceipt.transactionHash,
363+
destinationChainClient: destinationClient,
364+
fromBlock: destinationBlock - 100n, // TODO: use dynamic block buffer based chain
365+
});
366+
367+
const fillLog = parseFillLogs(fillTxReceipt.logs);
368+
369+
const fillSuccessProgress: TransactionProgress = {
370+
step: "fill",
371+
status: "txSuccess",
372+
txReceipt: fillTxReceipt,
373+
fillTxTimestamp,
374+
actionSuccess,
375+
fillLog,
376+
meta: fillMeta,
377+
};
378+
onProgressHandler(fillSuccessProgress);
379+
return { depositId, depositTxReceipt, fillTxReceipt };
380+
}
381+
} catch (error) {
382+
if (
383+
error instanceof Error &&
384+
error.message
385+
.toLowerCase()
386+
.includes("user rejected account upgrade")
387+
) {
388+
logger?.debug(
389+
"User rejected smart account upgrade, falling back to regular flow",
390+
);
391+
} else {
392+
throw error;
393+
}
394+
}
395+
}
396+
397+
// Fall back to regular approval flow
284398
const { request } = await simulateApproveTx({
285399
walletClient,
286400
publicClient: originClient,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
Hex,
3+
PublicClient,
4+
WalletClient,
5+
parseAbi,
6+
SimulateCallsReturnType,
7+
Call,
8+
encodeFunctionData,
9+
concatHex,
10+
} from "viem";
11+
import { Quote } from "./getQuote.js";
12+
import { LoggerT } from "../utils/index.js";
13+
import { spokePoolAbiV3_5 } from "../abis/SpokePool/index.js";
14+
import { addressToBytes32, getIntegratorDataSuffix } from "../utils/index.js";
15+
16+
export type PrepareAtomicApproveDepositTxParams = {
17+
walletClient: WalletClient;
18+
publicClient: PublicClient;
19+
deposit: Quote["deposit"];
20+
approvalAmount: bigint;
21+
integratorId: Hex;
22+
logger?: LoggerT;
23+
};
24+
25+
export type PrepareAtomicApproveDepositTxResult = {
26+
simulationResult?: SimulateCallsReturnType<readonly Call[]>;
27+
calls: readonly Call[];
28+
simulationError?: Error;
29+
};
30+
31+
export async function prepareAtomicApproveDepositTx(
32+
params: PrepareAtomicApproveDepositTxParams,
33+
): Promise<PrepareAtomicApproveDepositTxResult> {
34+
const {
35+
walletClient,
36+
publicClient,
37+
deposit,
38+
approvalAmount,
39+
integratorId,
40+
logger,
41+
} = params;
42+
43+
const account = walletClient.account;
44+
45+
if (!account) {
46+
throw new Error("Wallet account has to be set");
47+
}
48+
49+
const connectedChainId = await walletClient.getChainId();
50+
51+
if (connectedChainId !== deposit.originChainId) {
52+
throw new Error(
53+
`Connected chainId ${connectedChainId} does not match originChainId ${deposit.originChainId}`,
54+
);
55+
}
56+
57+
const calls: readonly Call[] = [
58+
{
59+
to: deposit.inputToken,
60+
abi: parseAbi([
61+
"function approve(address spender, uint256 amount) public returns (bool)",
62+
]),
63+
functionName: "approve",
64+
args: [deposit.spokePoolAddress, approvalAmount],
65+
},
66+
{
67+
to: deposit.spokePoolAddress,
68+
data: concatHex([
69+
encodeFunctionData({
70+
abi: spokePoolAbiV3_5,
71+
functionName: "deposit",
72+
args: [
73+
addressToBytes32(account.address),
74+
addressToBytes32(deposit.recipient ?? account.address),
75+
addressToBytes32(deposit.inputToken),
76+
addressToBytes32(deposit.outputToken),
77+
BigInt(deposit.inputAmount),
78+
deposit.outputAmount,
79+
BigInt(deposit.destinationChainId),
80+
addressToBytes32(deposit.exclusiveRelayer),
81+
deposit.quoteTimestamp,
82+
deposit.fillDeadline,
83+
deposit.exclusivityDeadline,
84+
deposit.message,
85+
],
86+
}),
87+
getIntegratorDataSuffix(integratorId),
88+
]),
89+
value: deposit.isNative ? BigInt(deposit.inputAmount) : 0n,
90+
},
91+
] as const;
92+
93+
try {
94+
const simulationResult = await publicClient.simulateCalls({
95+
account,
96+
calls,
97+
});
98+
99+
logger?.debug("Atomic transaction simulation result", simulationResult);
100+
101+
return {
102+
simulationResult: simulationResult,
103+
calls,
104+
};
105+
} catch (error) {
106+
logger?.debug("Atomic transaction simulation failed", error);
107+
return {
108+
simulationError: error as Error,
109+
calls,
110+
};
111+
}
112+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Hash, TransactionReceipt } from "viem";
2+
import { ConfiguredWalletClient } from "../types/index.js";
3+
import { getDepositFromLogs } from "./getDepositFromLogs.js";
4+
import { NoDepositLogError } from "../errors/index.js";
5+
6+
export type WaitForAtomicTxParams = {
7+
callId: string;
8+
originChainId: number;
9+
walletClient: ConfiguredWalletClient;
10+
};
11+
12+
export type AtomicTxStatus = {
13+
depositTxReceipt: TransactionReceipt;
14+
depositId: bigint;
15+
};
16+
17+
export async function waitForAtomicTx(
18+
params: WaitForAtomicTxParams,
19+
): Promise<AtomicTxStatus> {
20+
const { callId, originChainId, walletClient } = params;
21+
22+
const callResult = await walletClient.waitForCallsStatus({
23+
id: callId,
24+
});
25+
26+
if (!callResult?.receipts?.[0]) {
27+
throw new Error("No receipt returned from atomic transaction");
28+
}
29+
30+
if (callResult.receipts[0].status === "reverted") {
31+
throw new Error("Atomic transaction reverted");
32+
}
33+
34+
const receipt = callResult.receipts[0] as TransactionReceipt;
35+
36+
const depositLog = getDepositFromLogs({
37+
originChainId,
38+
receipt,
39+
});
40+
41+
if (!depositLog || !depositLog.depositId) {
42+
throw new NoDepositLogError(callId as Hash, originChainId);
43+
}
44+
45+
const depositId = depositLog.depositId;
46+
47+
return {
48+
depositTxReceipt: receipt,
49+
depositId,
50+
};
51+
}

0 commit comments

Comments
 (0)