tollkit-wallet 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/actions.d.ts +131 -35
- package/dist/ai.d.ts +13 -1
- package/dist/ai.js +2 -2
- package/dist/ai.js.map +1 -1
- package/dist/autopilot.d.ts +7 -0
- package/dist/autopilot.js +118 -3
- package/dist/autopilot.js.map +1 -1
- package/dist/bots.d.ts +92 -0
- package/dist/bots.js +183 -0
- package/dist/bots.js.map +1 -0
- package/dist/bridge.js +7 -5
- package/dist/bridge.js.map +1 -1
- package/dist/dapp.d.ts +11 -0
- package/dist/dapp.js +82 -14
- package/dist/dapp.js.map +1 -1
- package/dist/dashboard.d.ts +12 -0
- package/dist/dashboard.js +136 -14
- package/dist/dashboard.js.map +1 -1
- package/dist/data/token-list.d.ts +12 -0
- package/dist/data/token-list.js +13 -0
- package/dist/data/token-list.js.map +1 -0
- package/dist/evmsend.d.ts +30 -0
- package/dist/evmsend.js +47 -0
- package/dist/evmsend.js.map +1 -0
- package/dist/holdings.d.ts +6 -0
- package/dist/holdings.js +41 -8
- package/dist/holdings.js.map +1 -1
- package/dist/ledger.d.ts +1 -0
- package/dist/ledger.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/pay.js +1 -1
- package/dist/pay.js.map +1 -1
- package/dist/policy.d.ts +2 -2
- package/dist/rules.d.ts +57 -6
- package/dist/rules.js +46 -4
- package/dist/rules.js.map +1 -1
- package/dist/seller.js +15 -4
- package/dist/seller.js.map +1 -1
- package/dist/send.js +3 -2
- package/dist/send.js.map +1 -1
- package/dist/solconfirm.d.ts +13 -0
- package/dist/solconfirm.js +21 -0
- package/dist/solconfirm.js.map +1 -0
- package/dist/stake.js +3 -2
- package/dist/stake.js.map +1 -1
- package/dist/swap.d.ts +58 -5
- package/dist/swap.js +327 -21
- package/dist/swap.js.map +1 -1
- package/dist/tokenlist.d.ts +70 -0
- package/dist/tokenlist.js +259 -0
- package/dist/tokenlist.js.map +1 -0
- package/dist/trademode.d.ts +3 -0
- package/dist/trademode.js +23 -0
- package/dist/trademode.js.map +1 -0
- package/dist/ui.js +202 -19
- package/dist/ui.js.map +1 -1
- package/dist/wallet.d.ts +13 -1
- package/dist/wallet.js +85 -5
- package/dist/wallet.js.map +1 -1
- package/extension/background.js +1 -1
- package/extension/popup.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export async function sendAndConfirm(conn, tx, signers, o = {}) {
|
|
2
|
+
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash("confirmed");
|
|
3
|
+
tx.recentBlockhash = blockhash;
|
|
4
|
+
if (!tx.feePayer)
|
|
5
|
+
tx.feePayer = signers[0].publicKey;
|
|
6
|
+
tx.sign(...signers);
|
|
7
|
+
const sig = await conn.sendRawTransaction(tx.serialize(), { preflightCommitment: "confirmed", maxRetries: 3 });
|
|
8
|
+
const until = Date.now() + (o.maxMs ?? 90_000);
|
|
9
|
+
for (;;) {
|
|
10
|
+
const st = (await conn.getSignatureStatuses([sig])).value[0];
|
|
11
|
+
if (st?.err)
|
|
12
|
+
throw new Error(`Solana transaction ${sig} failed: ${JSON.stringify(st.err)}`);
|
|
13
|
+
if (st && (st.confirmationStatus === "confirmed" || st.confirmationStatus === "finalized"))
|
|
14
|
+
return sig;
|
|
15
|
+
if (Date.now() > until || (await conn.getBlockHeight("confirmed")) > lastValidBlockHeight) {
|
|
16
|
+
throw new Error(`Solana transaction ${sig} wasn't confirmed in time. Check it on an explorer before trying again.`);
|
|
17
|
+
}
|
|
18
|
+
await new Promise((r) => setTimeout(r, o.pollMs ?? 1500));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=solconfirm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"solconfirm.js","sourceRoot":"","sources":["../src/solconfirm.ts"],"names":[],"mappings":"AAUA,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAgB,EAAE,EAAe,EAAE,OAAkB,EAAE,IAAyC,EAAE;IACrI,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC;IACvF,EAAE,CAAC,eAAe,GAAG,SAAS,CAAC;IAC/B,IAAI,CAAC,EAAE,CAAC,QAAQ;QAAE,EAAE,CAAC,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC,SAAS,CAAC;IACtD,EAAE,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;IACpB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,EAAE,mBAAmB,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/G,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC;IAC/C,SAAS,CAAC;QACR,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC7D,IAAI,EAAE,EAAE,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,YAAY,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5F,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAkB,KAAK,WAAW,IAAI,EAAE,CAAC,kBAAkB,KAAK,WAAW,CAAC;YAAE,OAAO,GAAG,CAAC;QACvG,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,GAAG,oBAAoB,EAAE,CAAC;YAC1F,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,yEAAyE,CAAC,CAAC;QACtH,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC"}
|
package/dist/stake.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* Staking moves no money out of the wallet's control, so it isn't counted against spending limits,
|
|
14
14
|
* but it needs the password every time and agents can't reach it.
|
|
15
15
|
*/
|
|
16
|
-
import { Authorized, Connection, Lockup, PublicKey, StakeProgram, Transaction, ValidatorInfo
|
|
16
|
+
import { Authorized, Connection, Lockup, PublicKey, StakeProgram, Transaction, ValidatorInfo } from "@solana/web3.js";
|
|
17
|
+
import { sendAndConfirm } from "./solconfirm.js";
|
|
17
18
|
export const CONFIG_PROGRAM = new PublicKey("Config1111111111111111111111111111111111111");
|
|
18
19
|
const U64_MAX = "18446744073709551615";
|
|
19
20
|
const LAMPORTS = 1_000_000_000;
|
|
@@ -152,7 +153,7 @@ export class Staking {
|
|
|
152
153
|
send(tx, kp) {
|
|
153
154
|
if (this.o.send)
|
|
154
155
|
return this.o.send(tx, kp);
|
|
155
|
-
return
|
|
156
|
+
return sendAndConfirm(new Connection(this.o.rpc, "confirmed"), tx, [kp]);
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
//# sourceMappingURL=stake.js.map
|
package/dist/stake.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stake.js","sourceRoot":"","sources":["../src/stake.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"stake.js","sourceRoot":"","sources":["../src/stake.ts"],"names":[],"mappings":"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"}
|
package/dist/swap.d.ts
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* quote proxy (api.tollkit.dev/wallet/...), which holds the 0x and Jupiter API
|
|
5
5
|
* keys and adds the flat 0.5% fee; it only ever sees public data and signed
|
|
6
6
|
* transactions, never keys. The wallet checks every quote before signing:
|
|
7
|
-
* fee not above 0.5%, the only approval ever given is to 0x AllowanceHolder
|
|
8
|
-
*
|
|
7
|
+
* fee not above 0.5%, the only approval ever given is to 0x AllowanceHolder
|
|
8
|
+
* (exact amount), and the minimum output is set. It doesn't trust the quote
|
|
9
|
+
* server with what it signs (0.2.3): verifySwapTx decodes 0x calldata (exact
|
|
10
|
+
* token, amount and ETH value; the Settler call must pay this wallet the buy
|
|
11
|
+
* token with the minimum shown; Settler must be listed by 0x's on-chain
|
|
12
|
+
* registry, github.com/0xProject/0x-settler README), and simulates Solana
|
|
13
|
+
* swaps, refusing any that take more than the coin being sold.
|
|
9
14
|
*/
|
|
10
|
-
import { Connection, Keypair } from "@solana/web3.js";
|
|
11
|
-
import { type Wallet, type HDNodeWallet } from "ethers";
|
|
15
|
+
import { Connection, Keypair, PublicKey, VersionedTransaction } from "@solana/web3.js";
|
|
16
|
+
import { JsonRpcProvider, type Wallet, type HDNodeWallet } from "ethers";
|
|
12
17
|
import { type Chain } from "./chains.js";
|
|
13
18
|
type Fetch = typeof fetch;
|
|
14
19
|
export declare const DEFAULT_PROXY = "https://api.tollkit.dev/wallet";
|
|
@@ -31,21 +36,69 @@ export interface SwapQuote {
|
|
|
31
36
|
venue_fee_bps: number;
|
|
32
37
|
usd_value: number | null;
|
|
33
38
|
fee_usd: number | null;
|
|
39
|
+
/** Solana only: Jupiter's route doesn't carry Tollkit's fee, so the wallet sends it on its own, in SOL, after the swap. */
|
|
40
|
+
fee_separate?: boolean;
|
|
41
|
+
/** Solana only: built from Jupiter's instructions with Tollkit's fee (in SOL) inside the same transaction. */
|
|
42
|
+
fee_inline?: boolean;
|
|
34
43
|
raw: any;
|
|
35
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Jupiter pays Tollkit's 0.5% out of the route only when Tollkit has a fee account for the token Jupiter picks to
|
|
47
|
+
* take the fee in (it prefers SOL, then stablecoins, then other tokens). For any other token the order comes back
|
|
48
|
+
* with Jupiter's own default fee instead (developers.jup.ag/docs/swap/fees: "the order still returns but executes
|
|
49
|
+
* without your fees"), and Tollkit's fee is sent separately.
|
|
50
|
+
*/
|
|
51
|
+
export declare function jupiterFeeInRoute(raw: any): boolean;
|
|
52
|
+
/** The coin Tollkit's fee is taken in on Base and Ethereum: a stablecoin side first, then ETH or WETH, else the coin sold. */
|
|
53
|
+
export declare function feeTokenFor(chain: Chain, sell: string, buy: string): string;
|
|
54
|
+
/** Tollkit's 0.5% in lamports: straight from the SOL side when there is one, else the trade's dollar value at SOL's price. */
|
|
55
|
+
export declare function solFeeLamports(q: Pick<SwapQuote, "sell_token" | "buy_token" | "sell_amount" | "buy_amount">, usd: number | null, solUsd: number | null): bigint;
|
|
36
56
|
/** Dollar value of the sell side when either side is a stablecoin; else null (caller prices it). */
|
|
37
57
|
export declare function stableValue(q: Pick<SwapQuote, "chain" | "sell_token" | "buy_token" | "sell_amount" | "buy_amount">): number | null;
|
|
38
58
|
export declare function getQuote(req: SwapRequest, taker: string, opts?: {
|
|
39
59
|
fetch?: Fetch;
|
|
40
60
|
proxy?: string;
|
|
61
|
+
build?: boolean;
|
|
41
62
|
}): Promise<SwapQuote>;
|
|
42
63
|
/** Everything the wallet refuses to sign. Returns a reason, or null if the quote is safe. */
|
|
43
|
-
export declare function vetQuote(q: SwapQuote, req: SwapRequest): string | null;
|
|
64
|
+
export declare function vetQuote(q: SwapQuote, req: SwapRequest, taker?: string): string | null;
|
|
65
|
+
/** 0x Settler registry, the same address on every chain; feature 2 = taker-submitted swaps. */
|
|
66
|
+
export declare const SETTLER_REGISTRY = "0x00000000000004533Fe15556B1E086BB1A72cEae";
|
|
67
|
+
/** Decode a 0x quote's transaction and check it does only the swap shown. Returns the Settler address it uses. */
|
|
68
|
+
export declare function checkZeroExTx(q: SwapQuote, taker: string): string;
|
|
69
|
+
/** Programs a Solana swap may call directly. JUP6…: Jupiter's router (Metis), as seen in live Ultra orders. */
|
|
70
|
+
export declare const SWAP_PROGRAMS: Set<string>;
|
|
71
|
+
export interface VerifyDeps {
|
|
72
|
+
/** A Solana transaction the wallet assembled itself (Jupiter's router path), and the SOL it adds for Tollkit's fee. */ vtx?: VersionedTransaction;
|
|
73
|
+
extraLamports?: bigint;
|
|
74
|
+
/** eth_call on the swap's chain; returns the hex result. */ evmCall?: (chain: Chain, to: string, data: string) => Promise<string>;
|
|
75
|
+
/** Solana connection (tests pass a fake). */ conn?: Pick<Connection, "getBalance" | "getParsedTokenAccountsByOwner" | "simulateTransaction"> & {
|
|
76
|
+
getAddressLookupTable?: Connection["getAddressLookupTable"];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Before signing: check the quote server's transaction does only the swap that was shown. Throws if not. */
|
|
80
|
+
export declare function verifySwapTx(q: SwapQuote, me: {
|
|
81
|
+
evm: string;
|
|
82
|
+
solana: PublicKey;
|
|
83
|
+
}, d?: VerifyDeps & ExecDeps): Promise<void>;
|
|
44
84
|
export interface ExecDeps {
|
|
45
85
|
rpc?: Partial<Record<Chain, string>>;
|
|
46
86
|
fetch?: Fetch;
|
|
47
87
|
proxy?: string;
|
|
88
|
+
verify?: VerifyDeps; /** tests: the EVM provider to send with */
|
|
89
|
+
provider?: JsonRpcProvider; /** tests: how often to check a Solana send */
|
|
90
|
+
pollMs?: number;
|
|
48
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Jupiter's router instructions (Swap API v2 /build) plus Tollkit's fee as a plain SOL transfer, in one v0
|
|
94
|
+
* transaction: compute-unit limit from a simulation (x1.2, at most 1.4M), Jupiter's priority price, a fresh
|
|
95
|
+
* blockhash. Checked with the same rules as any swap, then sent and confirmed by polling.
|
|
96
|
+
*/
|
|
97
|
+
export declare function assembleBuilt(q: SwapQuote, payer: PublicKey, conn: Pick<Connection, "getLatestBlockhash" | "simulateTransaction">): Promise<{
|
|
98
|
+
tx: VersionedTransaction;
|
|
99
|
+
lastValidBlockHeight: number;
|
|
100
|
+
feeLamports: bigint;
|
|
101
|
+
}>;
|
|
49
102
|
/** Sign and send. Returns the transaction hash / signature. */
|
|
50
103
|
export declare function executeSwap(q: SwapQuote, keys: {
|
|
51
104
|
evm: Wallet | HDNodeWallet;
|
package/dist/swap.js
CHANGED
|
@@ -4,13 +4,53 @@
|
|
|
4
4
|
* quote proxy (api.tollkit.dev/wallet/...), which holds the 0x and Jupiter API
|
|
5
5
|
* keys and adds the flat 0.5% fee; it only ever sees public data and signed
|
|
6
6
|
* transactions, never keys. The wallet checks every quote before signing:
|
|
7
|
-
* fee not above 0.5%, the only approval ever given is to 0x AllowanceHolder
|
|
8
|
-
*
|
|
7
|
+
* fee not above 0.5%, the only approval ever given is to 0x AllowanceHolder
|
|
8
|
+
* (exact amount), and the minimum output is set. It doesn't trust the quote
|
|
9
|
+
* server with what it signs (0.2.3): verifySwapTx decodes 0x calldata (exact
|
|
10
|
+
* token, amount and ETH value; the Settler call must pay this wallet the buy
|
|
11
|
+
* token with the minimum shown; Settler must be listed by 0x's on-chain
|
|
12
|
+
* registry, github.com/0xProject/0x-settler README), and simulates Solana
|
|
13
|
+
* swaps, refusing any that take more than the coin being sold.
|
|
9
14
|
*/
|
|
10
|
-
import { Connection, VersionedTransaction } from "@solana/web3.js";
|
|
11
|
-
import { Contract, JsonRpcProvider } from "ethers";
|
|
12
|
-
import {
|
|
15
|
+
import { AddressLookupTableAccount, ComputeBudgetProgram, Connection, PublicKey, SystemProgram, TransactionInstruction, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
|
|
16
|
+
import { AbiCoder, Contract, Interface, JsonRpcProvider, ZeroAddress } from "ethers";
|
|
17
|
+
import { KNOWN } from "./tokens.js";
|
|
18
|
+
import { sendingProvider, waitMined } from "./evmsend.js";
|
|
19
|
+
import { ALLOWANCE_HOLDER, DEFAULT_RPC, EVM_CHAIN_ID, NATIVE_EVM, STABLES, SWAP_FEE_BPS, WSOL, isStable } from "./chains.js";
|
|
13
20
|
export const DEFAULT_PROXY = "https://api.tollkit.dev/wallet";
|
|
21
|
+
/**
|
|
22
|
+
* Jupiter pays Tollkit's 0.5% out of the route only when Tollkit has a fee account for the token Jupiter picks to
|
|
23
|
+
* take the fee in (it prefers SOL, then stablecoins, then other tokens). For any other token the order comes back
|
|
24
|
+
* with Jupiter's own default fee instead (developers.jup.ag/docs/swap/fees: "the order still returns but executes
|
|
25
|
+
* without your fees"), and Tollkit's fee is sent separately.
|
|
26
|
+
*/
|
|
27
|
+
export function jupiterFeeInRoute(raw) {
|
|
28
|
+
return !!raw?.referralAccount && Number(raw?.platformFee?.feeBps ?? raw?.feeBps) === SWAP_FEE_BPS;
|
|
29
|
+
}
|
|
30
|
+
/** The coin Tollkit's fee is taken in on Base and Ethereum: a stablecoin side first, then ETH or WETH, else the coin sold. */
|
|
31
|
+
export function feeTokenFor(chain, sell, buy) {
|
|
32
|
+
const eth = (t) => t.toLowerCase() === NATIVE_EVM.toLowerCase() || t.toLowerCase() === KNOWN[chain]?.WETH?.address?.toLowerCase();
|
|
33
|
+
if (isStable(chain, sell))
|
|
34
|
+
return sell;
|
|
35
|
+
if (isStable(chain, buy))
|
|
36
|
+
return buy;
|
|
37
|
+
if (eth(sell))
|
|
38
|
+
return sell;
|
|
39
|
+
if (eth(buy))
|
|
40
|
+
return buy;
|
|
41
|
+
return sell;
|
|
42
|
+
}
|
|
43
|
+
/** Tollkit's 0.5% in lamports: straight from the SOL side when there is one, else the trade's dollar value at SOL's price. */
|
|
44
|
+
export function solFeeLamports(q, usd, solUsd) {
|
|
45
|
+
const bps = BigInt(SWAP_FEE_BPS);
|
|
46
|
+
if (q.sell_token === WSOL)
|
|
47
|
+
return (BigInt(q.sell_amount) * bps) / 10000n;
|
|
48
|
+
if (q.buy_token === WSOL)
|
|
49
|
+
return (BigInt(q.buy_amount) * bps) / 10000n;
|
|
50
|
+
if (!(usd && usd > 0) || !(solUsd && solUsd > 0))
|
|
51
|
+
throw new Error("No SOL price right now to work out Tollkit's fee. Try again in a minute.");
|
|
52
|
+
return BigInt(Math.ceil(((usd * SWAP_FEE_BPS) / 10000 / solUsd) * 1e9));
|
|
53
|
+
}
|
|
14
54
|
const decimalsOf = (chain, token) => (chain === "solana" ? STABLES.solana[token] : STABLES[chain][token.toLowerCase()])?.decimals;
|
|
15
55
|
async function getJson(f, url, init) {
|
|
16
56
|
const r = await f(url, { ...init, signal: AbortSignal.timeout(20_000) });
|
|
@@ -33,23 +73,48 @@ export async function getQuote(req, taker, opts = {}) {
|
|
|
33
73
|
const u = new URL(`${proxy}/swap/solana/order`);
|
|
34
74
|
u.search = new URLSearchParams({ inputMint: req.sell_token, outputMint: req.buy_token, amount: req.sell_amount, taker, slippageBps: String(req.slippage_bps) }).toString();
|
|
35
75
|
const j = await getJson(f, u.toString());
|
|
76
|
+
const inRoute = jupiterFeeInRoute(j);
|
|
77
|
+
if (!inRoute && opts.build !== false) {
|
|
78
|
+
// No Tollkit fee account for the coin Jupiter would take the fee in: build the swap from Jupiter's router
|
|
79
|
+
// instructions instead, so Tollkit's 0.5% (in SOL) goes in the same transaction. If that path isn't
|
|
80
|
+
// available, fall back to the order with the fee sent right after (below).
|
|
81
|
+
try {
|
|
82
|
+
const bu = new URL(`${proxy}/swap/solana/build`);
|
|
83
|
+
bu.search = new URLSearchParams({ inputMint: req.sell_token, outputMint: req.buy_token, amount: req.sell_amount, taker, slippageBps: String(req.slippage_bps) }).toString();
|
|
84
|
+
const b = await getJson(f, bu.toString());
|
|
85
|
+
if (b?.swapInstruction && b?.tollkit_fee && typeof b.tollkit_fee.recipient === "string") {
|
|
86
|
+
return withUsd({
|
|
87
|
+
chain: "solana", venue: "jupiter", sell_token: req.sell_token, buy_token: req.buy_token,
|
|
88
|
+
sell_amount: String(b.inAmount ?? req.sell_amount), buy_amount: String(b.outAmount), min_buy_amount: String(b.otherAmountThreshold ?? b.outAmount),
|
|
89
|
+
fee_bps: Number(b.tollkit_fee.bps) === 0 ? 0 : SWAP_FEE_BPS, venue_fee_bps: 0, usd_value: null, fee_usd: null,
|
|
90
|
+
raw: { build: b, swapUsdValue: j.swapUsdValue, inUsdValue: j.inUsdValue }, fee_inline: true,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { /* the order below, with the fee sent separately */ }
|
|
95
|
+
}
|
|
36
96
|
const q = {
|
|
37
97
|
chain: "solana", venue: "jupiter", sell_token: req.sell_token, buy_token: req.buy_token,
|
|
38
98
|
sell_amount: String(j.inAmount ?? req.sell_amount), buy_amount: String(j.outAmount), min_buy_amount: String(j.otherAmountThreshold ?? j.outAmount),
|
|
39
|
-
|
|
99
|
+
// Tollkit's fee is always 0.5%; when the route doesn't carry it, whatever the route does carry is Jupiter's own.
|
|
100
|
+
fee_bps: SWAP_FEE_BPS, venue_fee_bps: inRoute ? 0 : Math.max(0, Number(j.platformFee?.feeBps ?? j.feeBps ?? 0) || 0),
|
|
101
|
+
usd_value: null, fee_usd: null, raw: j, ...(inRoute ? {} : { fee_separate: true }),
|
|
40
102
|
};
|
|
41
103
|
return withUsd(q);
|
|
42
104
|
}
|
|
43
105
|
const u = new URL(`${proxy}/swap/evm/quote`);
|
|
44
|
-
u.search = new URLSearchParams({ chainId: String(EVM_CHAIN_ID[req.chain]), sellToken: req.sell_token, buyToken: req.buy_token, sellAmount: req.sell_amount, taker, slippageBps: String(req.slippage_bps) }).toString();
|
|
106
|
+
u.search = new URLSearchParams({ chainId: String(EVM_CHAIN_ID[req.chain]), sellToken: req.sell_token, buyToken: req.buy_token, sellAmount: req.sell_amount, taker, slippageBps: String(req.slippage_bps), feeToken: feeTokenFor(req.chain, req.sell_token, req.buy_token) }).toString();
|
|
45
107
|
const j = await getJson(f, u.toString());
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const
|
|
108
|
+
const sell = BigInt(j.sellAmount ?? req.sell_amount), buy = BigInt(j.buyAmount ?? "0");
|
|
109
|
+
// A fee is a share of the side it's taken from: the amount sold, or what was bought before the fee came off.
|
|
110
|
+
const share = (fee) => {
|
|
111
|
+
const amt = BigInt(fee?.amount ?? "0"), tok = String(fee?.token ?? req.sell_token).toLowerCase();
|
|
112
|
+
const base = tok === req.buy_token.toLowerCase() ? buy + amt : tok === req.sell_token.toLowerCase() ? sell : 0n;
|
|
113
|
+
return base > 0n ? Number((amt * 20000n / base + 1n) / 2n) : 0;
|
|
114
|
+
};
|
|
115
|
+
const feeBps = share(j?.fees?.integratorFee);
|
|
49
116
|
// 0x's own fee (0.15% on select pairs on the Standard plan), shown to the user too.
|
|
50
|
-
const
|
|
51
|
-
const zxToken = String(j?.fees?.zeroExFee?.token ?? "").toLowerCase();
|
|
52
|
-
const venueBps = sell > 0n && zxToken === req.sell_token.toLowerCase() ? Number((zx * 10000n) / sell) : 0;
|
|
117
|
+
const venueBps = j?.fees?.zeroExFee ? share(j.fees.zeroExFee) : 0;
|
|
53
118
|
return withUsd({
|
|
54
119
|
chain: req.chain, venue: "0x", sell_token: req.sell_token, buy_token: req.buy_token,
|
|
55
120
|
sell_amount: String(j.sellAmount ?? req.sell_amount), buy_amount: String(j.buyAmount), min_buy_amount: String(j.minBuyAmount ?? j.buyAmount),
|
|
@@ -61,7 +126,9 @@ function withUsd(q) {
|
|
|
61
126
|
return { ...q, usd_value: usd, fee_usd: usd === null ? null : (usd * q.fee_bps) / 10000 };
|
|
62
127
|
}
|
|
63
128
|
/** Everything the wallet refuses to sign. Returns a reason, or null if the quote is safe. */
|
|
64
|
-
export function vetQuote(q, req) {
|
|
129
|
+
export function vetQuote(q, req, taker) {
|
|
130
|
+
if (q.sell_amount !== req.sell_amount)
|
|
131
|
+
return "The quote is for a different amount than requested.";
|
|
65
132
|
if (q.fee_bps > SWAP_FEE_BPS)
|
|
66
133
|
return `The quote carries a ${q.fee_bps / 100}% fee; Tollkit's fee is ${SWAP_FEE_BPS / 100}%.`;
|
|
67
134
|
if (!(BigInt(q.min_buy_amount) > 0n))
|
|
@@ -75,18 +142,256 @@ export function vetQuote(q, req) {
|
|
|
75
142
|
const spender = q.raw?.issues?.allowance?.spender;
|
|
76
143
|
if (spender && String(spender).toLowerCase() !== ALLOWANCE_HOLDER.toLowerCase())
|
|
77
144
|
return `The quote asks to approve ${spender}; only 0x AllowanceHolder is ever approved.`;
|
|
78
|
-
if (
|
|
79
|
-
|
|
145
|
+
if (taker && tx.data !== undefined) {
|
|
146
|
+
try {
|
|
147
|
+
checkZeroExTx(q, taker);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
return e.message;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
80
153
|
}
|
|
81
154
|
else {
|
|
82
|
-
if (
|
|
155
|
+
if (q.raw?.build) {
|
|
156
|
+
if (!q.raw.build.swapInstruction)
|
|
157
|
+
return "The quote has no swap instruction.";
|
|
158
|
+
}
|
|
159
|
+
else if (!q.raw?.transaction || !q.raw?.requestId)
|
|
83
160
|
return "The quote has no transaction.";
|
|
84
161
|
}
|
|
85
162
|
return null;
|
|
86
163
|
}
|
|
87
164
|
const ERC20 = ["function approve(address spender, uint256 amount) returns (bool)", "function allowance(address owner, address spender) view returns (uint256)"];
|
|
165
|
+
/** 0x Settler registry, the same address on every chain; feature 2 = taker-submitted swaps. */
|
|
166
|
+
export const SETTLER_REGISTRY = "0x00000000000004533Fe15556B1E086BB1A72cEae";
|
|
167
|
+
const ALLOWANCE_EXEC = new Interface(["function exec(address operator, address token, uint256 amount, address target, bytes data)"]);
|
|
168
|
+
const SETTLER_EXECUTE = "0x1fff991f"; // execute((address recipient, address buyToken, uint256 minAmountOut), bytes[], bytes32)
|
|
169
|
+
/** Decode a 0x quote's transaction and check it does only the swap shown. Returns the Settler address it uses. */
|
|
170
|
+
export function checkZeroExTx(q, taker) {
|
|
171
|
+
const t = q.raw?.transaction ?? {};
|
|
172
|
+
const native = q.sell_token.toLowerCase() === NATIVE_EVM.toLowerCase();
|
|
173
|
+
const value = BigInt(t.value ?? "0"), sell = BigInt(q.sell_amount);
|
|
174
|
+
if (String(t.to ?? "").toLowerCase() !== ALLOWANCE_HOLDER.toLowerCase())
|
|
175
|
+
throw new Error("The swap isn't sent to 0x AllowanceHolder.");
|
|
176
|
+
if (value !== (native ? sell : 0n))
|
|
177
|
+
throw new Error("The swap would send a different amount of ETH than you're selling.");
|
|
178
|
+
let p = null;
|
|
179
|
+
try {
|
|
180
|
+
p = ALLOWANCE_EXEC.parseTransaction({ data: String(t.data ?? "") });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
p = null;
|
|
184
|
+
}
|
|
185
|
+
if (!p || p.name !== "exec")
|
|
186
|
+
throw new Error("The swap transaction isn't a 0x swap.");
|
|
187
|
+
const [operator, token, amount, target, inner] = p.args;
|
|
188
|
+
if (token.toLowerCase() !== (native ? ZeroAddress : q.sell_token).toLowerCase() || amount !== sell)
|
|
189
|
+
throw new Error("The swap would take a different coin or amount than you're selling.");
|
|
190
|
+
if (operator.toLowerCase() !== target.toLowerCase())
|
|
191
|
+
throw new Error("The swap transaction isn't a 0x swap.");
|
|
192
|
+
if (!inner.startsWith(SETTLER_EXECUTE) || inner.length < 10 + 64 * 3)
|
|
193
|
+
throw new Error("The swap transaction isn't a 0x swap.");
|
|
194
|
+
const [recipient, buyToken, minOut] = AbiCoder.defaultAbiCoder().decode(["address", "address", "uint256"], "0x" + inner.slice(10, 10 + 64 * 3));
|
|
195
|
+
if (recipient.toLowerCase() !== taker.toLowerCase())
|
|
196
|
+
throw new Error("The swap would pay someone other than this wallet.");
|
|
197
|
+
if (buyToken.toLowerCase() !== q.buy_token.toLowerCase())
|
|
198
|
+
throw new Error("The swap would buy a different coin.");
|
|
199
|
+
if (minOut !== BigInt(q.min_buy_amount))
|
|
200
|
+
throw new Error("The swap's minimum doesn't match the quote shown.");
|
|
201
|
+
return target;
|
|
202
|
+
}
|
|
203
|
+
const TOKEN_PROGRAMS = ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"];
|
|
204
|
+
const ATA_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
205
|
+
/** Programs a Solana swap may call directly. JUP6…: Jupiter's router (Metis), as seen in live Ultra orders. */
|
|
206
|
+
export const SWAP_PROGRAMS = new Set(["ComputeBudget111111111111111111111111111111", "11111111111111111111111111111111", ...TOKEN_PROGRAMS, ATA_PROGRAM, "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"]);
|
|
207
|
+
/** Before signing: check the quote server's transaction does only the swap that was shown. Throws if not. */
|
|
208
|
+
export async function verifySwapTx(q, me, d = {}) {
|
|
209
|
+
if (q.venue === "0x") {
|
|
210
|
+
const settler = checkZeroExTx(q, me.evm).toLowerCase();
|
|
211
|
+
const call = d.evmCall ?? ((chain, to, data) => new JsonRpcProvider(d.rpc?.[chain] ?? DEFAULT_RPC[chain], EVM_CHAIN_ID[chain], { staticNetwork: true }).call({ to, data }));
|
|
212
|
+
const reg = async (sel) => { try {
|
|
213
|
+
return String(AbiCoder.defaultAbiCoder().decode(["address"], await call(q.chain, SETTLER_REGISTRY, sel + "0".repeat(63) + "2"))[0]).toLowerCase();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return "";
|
|
217
|
+
} };
|
|
218
|
+
// ownerOf(2) is the current Settler; prev(2) the one before, still used while 0x's API catches up.
|
|
219
|
+
if ((await reg("0x6352211e")) !== settler && (await reg("0xe2603dc2")) !== settler)
|
|
220
|
+
throw new Error("The swap goes through a contract 0x's registry doesn't list. Not signing it.");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
let vtx;
|
|
224
|
+
if (d.vtx)
|
|
225
|
+
vtx = d.vtx;
|
|
226
|
+
else
|
|
227
|
+
try {
|
|
228
|
+
vtx = VersionedTransaction.deserialize(Buffer.from(String(q.raw?.transaction ?? ""), "base64"));
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
throw new Error("The quote's transaction can't be read.");
|
|
232
|
+
}
|
|
233
|
+
const signers = vtx.message.staticAccountKeys.slice(0, vtx.message.header.numRequiredSignatures);
|
|
234
|
+
if (!signers.some((k) => k.equals(me.solana)))
|
|
235
|
+
throw new Error("The quote's transaction isn't for this wallet.");
|
|
236
|
+
const conn = d.conn ?? new Connection(d.rpc?.solana ?? DEFAULT_RPC.solana, "confirmed");
|
|
237
|
+
const fail = () => new Error("Couldn't check this swap before signing. Try again in a moment.");
|
|
238
|
+
// Only programs whose behaviour is known: Jupiter's own router (Metis, seen in live orders 2026-10-14) and the
|
|
239
|
+
// Solana programs a swap needs. A route through anything else is refused rather than trusted.
|
|
240
|
+
const statics = vtx.message.staticAccountKeys;
|
|
241
|
+
for (const ix of vtx.message.compiledInstructions) {
|
|
242
|
+
const prog = statics[ix.programIdIndex]?.toBase58();
|
|
243
|
+
if (!prog || !SWAP_PROGRAMS.has(prog))
|
|
244
|
+
throw new Error("This swap route goes through a program Tollkit doesn't sign for. Try again in a moment or change the amount.");
|
|
245
|
+
if (prog === "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" || prog === "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") {
|
|
246
|
+
// Approve, ApproveChecked, SetAuthority: never part of a swap.
|
|
247
|
+
if ([4, 6, 13].includes(ix.data[0]))
|
|
248
|
+
throw new Error("This swap would change who controls your tokens. Not signing it.");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const held = new Map();
|
|
252
|
+
try {
|
|
253
|
+
for (const prog of TOKEN_PROGRAMS) {
|
|
254
|
+
const r = await conn.getParsedTokenAccountsByOwner(me.solana, { programId: new PublicKey(prog) });
|
|
255
|
+
for (const a of r.value) {
|
|
256
|
+
const i = a.account.data.parsed?.info;
|
|
257
|
+
if (i)
|
|
258
|
+
held.set(a.pubkey.toBase58(), { mint: i.mint, amount: BigInt(i.tokenAmount.amount), delegate: i.delegate ?? null, close: i.closeAuthority ?? null });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
throw fail();
|
|
264
|
+
}
|
|
265
|
+
const luts = [];
|
|
266
|
+
for (const l of vtx.message.addressTableLookups ?? []) {
|
|
267
|
+
const t = await conn.getAddressLookupTable?.(l.accountKey).catch(() => null);
|
|
268
|
+
if (!t?.value)
|
|
269
|
+
throw fail();
|
|
270
|
+
luts.push(t.value);
|
|
271
|
+
}
|
|
272
|
+
const keys = vtx.message.getAccountKeys({ addressLookupTableAccounts: luts });
|
|
273
|
+
const watch = [];
|
|
274
|
+
for (let i = 0; i < keys.length; i++) {
|
|
275
|
+
const k = keys.get(i).toBase58();
|
|
276
|
+
if (held.has(k) && vtx.message.isAccountWritable(i))
|
|
277
|
+
watch.push(k);
|
|
278
|
+
}
|
|
279
|
+
// The account the bought coin lands in, even if it doesn't exist yet (then it starts at 0).
|
|
280
|
+
const buyAtas = q.buy_token === WSOL ? [] : TOKEN_PROGRAMS.map((p) => ataOf(me.solana, new PublicKey(q.buy_token), new PublicKey(p))).filter((a) => !watch.includes(a));
|
|
281
|
+
const addresses = [me.solana.toBase58(), ...watch, ...buyAtas];
|
|
282
|
+
if (addresses.length > 128)
|
|
283
|
+
throw fail();
|
|
284
|
+
let sim;
|
|
285
|
+
try {
|
|
286
|
+
sim = await conn.simulateTransaction(vtx, { sigVerify: false, replaceRecentBlockhash: true, accounts: { encoding: "base64", addresses } });
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
throw fail();
|
|
290
|
+
}
|
|
291
|
+
const accs = sim?.value?.accounts;
|
|
292
|
+
if (sim?.value?.err || !accs?.[0])
|
|
293
|
+
throw fail();
|
|
294
|
+
const before = await conn.getBalance(me.solana);
|
|
295
|
+
if (accs[0].owner && accs[0].owner !== "11111111111111111111111111111111")
|
|
296
|
+
throw new Error("This swap would hand your wallet account to another program. Not signing it.");
|
|
297
|
+
const tokens = new Map();
|
|
298
|
+
const readTok = (a) => { const b = Buffer.from(String(a?.data?.[0] ?? ""), "base64"); return b.length >= 165 ? b : null; };
|
|
299
|
+
watch.forEach((addr, i) => {
|
|
300
|
+
const w = held.get(addr), a = accs[i + 1];
|
|
301
|
+
if (!a) {
|
|
302
|
+
tokens.set(w.mint, (tokens.get(w.mint) ?? 0n) - w.amount);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const b = readTok(a);
|
|
306
|
+
if (!b)
|
|
307
|
+
return;
|
|
308
|
+
if (!b.subarray(32, 64).equals(me.solana.toBuffer()))
|
|
309
|
+
throw new Error("This swap would give one of your token accounts to someone else. Not signing it.");
|
|
310
|
+
const delegate = b.readUInt32LE(72) === 1 ? new PublicKey(b.subarray(76, 108)).toBase58() : null;
|
|
311
|
+
if (delegate && delegate !== w.delegate)
|
|
312
|
+
throw new Error("This swap would let another account spend your tokens later. Not signing it.");
|
|
313
|
+
const close = b.readUInt32LE(129) === 1 ? new PublicKey(b.subarray(133, 165)).toBase58() : null;
|
|
314
|
+
if (close && close !== w.close)
|
|
315
|
+
throw new Error("This swap would let another account close one of your token accounts. Not signing it.");
|
|
316
|
+
tokens.set(w.mint, (tokens.get(w.mint) ?? 0n) + b.readBigUInt64LE(64) - w.amount);
|
|
317
|
+
});
|
|
318
|
+
buyAtas.forEach((_addr, i) => {
|
|
319
|
+
const b = readTok(accs[watch.length + 1 + i]);
|
|
320
|
+
if (!b || !b.subarray(32, 64).equals(me.solana.toBuffer()) || new PublicKey(b.subarray(0, 32)).toBase58() !== q.buy_token)
|
|
321
|
+
return;
|
|
322
|
+
tokens.set(q.buy_token, (tokens.get(q.buy_token) ?? 0n) + b.readBigUInt64LE(64));
|
|
323
|
+
});
|
|
324
|
+
const sell = BigInt(q.sell_amount), sellingSol = q.sell_token === WSOL, buyingSol = q.buy_token === WSOL;
|
|
325
|
+
const solDelta = BigInt(accs[0].lamports) - BigInt(before) + (tokens.get(WSOL) ?? 0n);
|
|
326
|
+
// Network fees, priority fees and a new token account's deposit stay under 0.01 SOL.
|
|
327
|
+
// Plus Tollkit's fee when the wallet put it in this transaction itself.
|
|
328
|
+
const FEES = 10000000n + (d.extraLamports ?? 0n);
|
|
329
|
+
if (sellingSol && -solDelta > sell + FEES)
|
|
330
|
+
throw new Error("This swap would take more SOL than you're selling. Not signing it.");
|
|
331
|
+
if (!sellingSol && !buyingSol && -solDelta > FEES)
|
|
332
|
+
throw new Error("This swap would take more SOL than network fees. Not signing it.");
|
|
333
|
+
for (const [mint, delta] of tokens) {
|
|
334
|
+
if (delta >= 0n || mint === WSOL)
|
|
335
|
+
continue;
|
|
336
|
+
if (mint !== q.sell_token || -delta > sell)
|
|
337
|
+
throw new Error("This swap would take a coin you aren't selling, or more of it. Not signing it.");
|
|
338
|
+
}
|
|
339
|
+
const min = BigInt(q.min_buy_amount);
|
|
340
|
+
const got = buyingSol ? solDelta + FEES : (tokens.get(q.buy_token) ?? 0n);
|
|
341
|
+
if (got < min)
|
|
342
|
+
throw new Error("The checked swap pays less than the minimum shown. Not signing it.");
|
|
343
|
+
}
|
|
344
|
+
/** Associated token account address (the ATA program's seeds: owner, token program, mint). */
|
|
345
|
+
function ataOf(owner, mint, tokenProgram) {
|
|
346
|
+
return PublicKey.findProgramAddressSync([owner.toBuffer(), tokenProgram.toBuffer(), mint.toBuffer()], new PublicKey(ATA_PROGRAM))[0].toBase58();
|
|
347
|
+
}
|
|
348
|
+
const toIx = (i) => new TransactionInstruction({ programId: new PublicKey(i.programId), keys: (i.accounts ?? []).map((a) => ({ pubkey: new PublicKey(a.pubkey), isSigner: !!a.isSigner, isWritable: !!a.isWritable })), data: Buffer.from(String(i.data ?? ""), "base64") });
|
|
349
|
+
const COMPUTE_BUDGET = "ComputeBudget111111111111111111111111111111";
|
|
350
|
+
/**
|
|
351
|
+
* Jupiter's router instructions (Swap API v2 /build) plus Tollkit's fee as a plain SOL transfer, in one v0
|
|
352
|
+
* transaction: compute-unit limit from a simulation (x1.2, at most 1.4M), Jupiter's priority price, a fresh
|
|
353
|
+
* blockhash. Checked with the same rules as any swap, then sent and confirmed by polling.
|
|
354
|
+
*/
|
|
355
|
+
export async function assembleBuilt(q, payer, conn) {
|
|
356
|
+
const b = q.raw.build, fee = q.raw.fee;
|
|
357
|
+
const feeLamports = fee ? BigInt(fee.lamports) : 0n;
|
|
358
|
+
const price = (b.computeBudgetInstructions ?? []).map(toIx).filter((ix) => !(ix.programId.toBase58() === COMPUTE_BUDGET && ix.data[0] === 2));
|
|
359
|
+
const body = [...(b.setupInstructions ?? []).map(toIx), toIx(b.swapInstruction), ...(b.cleanupInstruction ? [toIx(b.cleanupInstruction)] : []), ...(b.otherInstructions ?? []).map(toIx)];
|
|
360
|
+
if (feeLamports > 0n)
|
|
361
|
+
body.push(SystemProgram.transfer({ fromPubkey: payer, toPubkey: new PublicKey(fee.to), lamports: feeLamports }));
|
|
362
|
+
const luts = Object.entries(b.addressesByLookupTableAddress ?? {}).map(([key, addrs]) => new AddressLookupTableAccount({ key: new PublicKey(key), state: { deactivationSlot: 18446744073709551615n, lastExtendedSlot: 0, lastExtendedSlotStartIndex: 0, addresses: addrs.map((a) => new PublicKey(a)) } }));
|
|
363
|
+
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash("confirmed");
|
|
364
|
+
const compile = (units) => new VersionedTransaction(new TransactionMessage({ payerKey: payer, recentBlockhash: blockhash, instructions: [ComputeBudgetProgram.setComputeUnitLimit({ units }), ...price, ...body] }).compileToV0Message(luts));
|
|
365
|
+
const sim = await conn.simulateTransaction(compile(1_400_000), { sigVerify: false, replaceRecentBlockhash: true });
|
|
366
|
+
if (sim.value.err)
|
|
367
|
+
throw new Error(`This swap would fail right now (${JSON.stringify(sim.value.err)}). Try again in a moment.`);
|
|
368
|
+
const units = Math.min(1_400_000, Math.ceil((sim.value.unitsConsumed ?? 1_200_000) * 1.2));
|
|
369
|
+
return { tx: compile(units), lastValidBlockHeight, feeLamports };
|
|
370
|
+
}
|
|
371
|
+
async function executeBuilt(q, keys, deps) {
|
|
372
|
+
const conn = deps.verify?.conn ?? new Connection(deps.rpc?.solana ?? DEFAULT_RPC.solana, "confirmed");
|
|
373
|
+
const { tx, lastValidBlockHeight, feeLamports } = await assembleBuilt(q, keys.solana.publicKey, conn);
|
|
374
|
+
await verifySwapTx(q, { evm: "", solana: keys.solana.publicKey }, { ...deps, ...deps.verify, vtx: tx, extraLamports: feeLamports });
|
|
375
|
+
tx.sign([keys.solana]);
|
|
376
|
+
const sig = await conn.sendRawTransaction(tx.serialize(), { preflightCommitment: "confirmed", maxRetries: 3 });
|
|
377
|
+
const until = Date.now() + 90_000;
|
|
378
|
+
for (;;) {
|
|
379
|
+
const st = (await conn.getSignatureStatuses([sig])).value[0];
|
|
380
|
+
if (st?.err)
|
|
381
|
+
throw new Error(`swap ${sig} failed: ${JSON.stringify(st.err)}`);
|
|
382
|
+
if (st && (st.confirmationStatus === "confirmed" || st.confirmationStatus === "finalized"))
|
|
383
|
+
return sig;
|
|
384
|
+
if (Date.now() > until || (await conn.getBlockHeight("confirmed")) > lastValidBlockHeight)
|
|
385
|
+
throw new Error(`Solana swap ${sig} wasn't confirmed in time. Check it on an explorer before trying again.`);
|
|
386
|
+
await new Promise((r) => setTimeout(r, deps.pollMs ?? 1500));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
88
389
|
/** Sign and send. Returns the transaction hash / signature. */
|
|
89
390
|
export async function executeSwap(q, keys, deps = {}) {
|
|
391
|
+
// A built Solana swap is assembled first, then checked as the exact transaction that will be signed.
|
|
392
|
+
if (q.venue === "jupiter" && q.raw?.build)
|
|
393
|
+
return executeBuilt(q, keys, deps);
|
|
394
|
+
await verifySwapTx(q, { evm: keys.evm.address, solana: keys.solana.publicKey }, { ...deps, ...deps.verify });
|
|
90
395
|
if (q.venue === "jupiter") {
|
|
91
396
|
const tx = VersionedTransaction.deserialize(Buffer.from(q.raw.transaction, "base64"));
|
|
92
397
|
tx.sign([keys.solana]);
|
|
@@ -99,19 +404,20 @@ export async function executeSwap(q, keys, deps = {}) {
|
|
|
99
404
|
throw new Error(`swap failed: ${j.error ?? j.status}`);
|
|
100
405
|
return String(j.signature);
|
|
101
406
|
}
|
|
102
|
-
|
|
407
|
+
// On Ethereum mainnet the approval and the swap go through Flashbots Protect (src/evmsend.ts).
|
|
408
|
+
const provider = deps.provider ?? sendingProvider(q.chain, { rpc: deps.rpc, proxy: deps.proxy, fetch: deps.fetch });
|
|
103
409
|
const signer = keys.evm.connect(provider);
|
|
104
410
|
if (q.raw?.issues?.allowance) {
|
|
105
411
|
const token = new Contract(q.sell_token, ERC20, signer);
|
|
106
|
-
const a = await token.approve(ALLOWANCE_HOLDER, BigInt(q.sell_amount)); // exact amount, never unlimited
|
|
107
|
-
await a
|
|
412
|
+
const a = await token.getFunction("approve")(ALLOWANCE_HOLDER, BigInt(q.sell_amount)); // exact amount, never unlimited
|
|
413
|
+
await waitMined(a, "approval");
|
|
108
414
|
}
|
|
109
415
|
const t = q.raw.transaction;
|
|
110
416
|
const sent = await signer.sendTransaction({
|
|
111
417
|
to: t.to, data: t.data, value: BigInt(t.value ?? "0"),
|
|
112
418
|
...(t.gas ? { gasLimit: BigInt(t.gas) } : {}),
|
|
113
419
|
});
|
|
114
|
-
const r = await sent
|
|
420
|
+
const r = await waitMined(sent, "swap");
|
|
115
421
|
if (!r || r.status !== 1)
|
|
116
422
|
throw new Error(`swap transaction ${sent.hash} reverted`);
|
|
117
423
|
return sent.hash;
|