damm-sdk 1.4.20 → 1.4.22

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.
@@ -0,0 +1,172 @@
1
+ import { ethers } from "ethers";
2
+ import ccipRouterAbi from "./ccip.router.abi";
3
+ import type { Call, HexString, Unwrapable } from "../../types";
4
+ import { createCall } from "../../types";
5
+ import type { Address } from "viem";
6
+
7
+ const ccipRouterInterface = new ethers.utils.Interface(ccipRouterAbi as any);
8
+
9
+ /**
10
+ * CCIP chain selectors (uint64).
11
+ * Source: https://docs.chain.link/ccip/directory/mainnet
12
+ * Verified on-chain 2026-04-13 via typeAndVersion() on each Router.
13
+ */
14
+ export const CCIP_CHAIN_SELECTORS: Readonly<Record<number, string>> = Object.freeze({
15
+ 1: "5009297550715157269", // Mainnet
16
+ 42161: "4949039107694359620", // Arbitrum
17
+ 8453: "15971525489660198786", // Base
18
+ 10: "3734403246176062136", // Optimism
19
+ 137: "4051577828743386545", // Polygon
20
+ 56: "11344663589394136015", // BSC
21
+ 143: "8481857512324358265", // Monad
22
+ });
23
+
24
+ /**
25
+ * Get the CCIP chain selector for a given chain ID.
26
+ * @throws if the chain is not supported
27
+ */
28
+ export const getCCIPChainSelector = (chainId: number): string => {
29
+ const selector = CCIP_CHAIN_SELECTORS[chainId];
30
+ if (!selector) {
31
+ throw new Error(`CCIP: No chain selector for chainId ${chainId}`);
32
+ }
33
+ return selector;
34
+ };
35
+
36
+ // ─── EVM2AnyMessage struct ───────────────────────────────────────────────
37
+
38
+ export type CCIPTokenAmount = Readonly<{
39
+ token: Address;
40
+ amount: bigint;
41
+ }>;
42
+
43
+ export type EVM2AnyMessage = Readonly<{
44
+ /** abi.encode(destinationAddress) */
45
+ receiver: HexString;
46
+ /** Empty (0x) for token-only transfers */
47
+ data: HexString;
48
+ /** Tokens to bridge */
49
+ tokenAmounts: readonly CCIPTokenAmount[];
50
+ /** address(0) = pay fee in native ETH/MON */
51
+ feeToken: Address;
52
+ /** Extra args (0x for defaults) */
53
+ extraArgs: HexString;
54
+ }>;
55
+
56
+ // ─── getFee ──────────────────────────────────────────────────────────────
57
+
58
+ export type GetFeeArgs = Readonly<{
59
+ destinationChainSelector: string;
60
+ message: EVM2AnyMessage;
61
+ }>;
62
+
63
+ /**
64
+ * Encode calldata for Router.getFee(destinationChainSelector, message).
65
+ * Use this to quote the CCIP fee via an RPC call.
66
+ */
67
+ export const CCIPGetFeeCalldata = (args: GetFeeArgs): HexString => {
68
+ return ccipRouterInterface.encodeFunctionData("getFee", [
69
+ args.destinationChainSelector,
70
+ {
71
+ receiver: args.message.receiver,
72
+ data: args.message.data,
73
+ tokenAmounts: args.message.tokenAmounts.map((t) => ({
74
+ token: t.token,
75
+ amount: t.amount,
76
+ })),
77
+ feeToken: args.message.feeToken,
78
+ extraArgs: args.message.extraArgs,
79
+ },
80
+ ]) as HexString;
81
+ };
82
+
83
+ /**
84
+ * Build a Call for Router.getFee (read-only, value=0).
85
+ */
86
+ export const CCIPGetFeeTrx = ({
87
+ routerAddress,
88
+ args,
89
+ }: {
90
+ routerAddress: Address;
91
+ args: GetFeeArgs;
92
+ }): Unwrapable<Call> => {
93
+ return createCall({
94
+ operation: 0,
95
+ to: routerAddress,
96
+ data: CCIPGetFeeCalldata(args),
97
+ value: 0n,
98
+ });
99
+ };
100
+
101
+ // ─── ccipSend ────────────────────────────────────────────────────────────
102
+
103
+ export type CCIPSendArgs = Readonly<{
104
+ destinationChainSelector: string;
105
+ message: EVM2AnyMessage;
106
+ /** Native fee (from getFee). The Router does NOT refund excess. */
107
+ nativeFee: bigint;
108
+ }>;
109
+
110
+ /**
111
+ * Encode calldata for Router.ccipSend(destinationChainSelector, message).
112
+ */
113
+ export const CCIPSendCalldata = (args: CCIPSendArgs): HexString => {
114
+ return ccipRouterInterface.encodeFunctionData("ccipSend", [
115
+ args.destinationChainSelector,
116
+ {
117
+ receiver: args.message.receiver,
118
+ data: args.message.data,
119
+ tokenAmounts: args.message.tokenAmounts.map((t) => ({
120
+ token: t.token,
121
+ amount: t.amount,
122
+ })),
123
+ feeToken: args.message.feeToken,
124
+ extraArgs: args.message.extraArgs,
125
+ },
126
+ ]) as HexString;
127
+ };
128
+
129
+ /**
130
+ * Build a Call for Router.ccipSend with msg.value = nativeFee.
131
+ */
132
+ export const ccipSendTrx = ({
133
+ routerAddress,
134
+ args,
135
+ }: {
136
+ routerAddress: Address;
137
+ args: CCIPSendArgs;
138
+ }): Unwrapable<Call> => {
139
+ return createCall({
140
+ operation: 0,
141
+ to: routerAddress,
142
+ data: CCIPSendCalldata(args),
143
+ value: args.nativeFee,
144
+ });
145
+ };
146
+
147
+ // ─── Helper: build EVM2AnyMessage for a single token bridge ──────────────
148
+
149
+ /**
150
+ * Build a standard EVM2AnyMessage for bridging a single token.
151
+ * Receiver = abi.encode(destinationAddress), feeToken = address(0) (native).
152
+ */
153
+ export const buildCCIPMessage = ({
154
+ receiver,
155
+ token,
156
+ amount,
157
+ }: {
158
+ receiver: Address;
159
+ token: Address;
160
+ amount: bigint;
161
+ }): EVM2AnyMessage => {
162
+ // Pad receiver to 32 bytes (abi.encode(address))
163
+ const receiverEncoded = ("0x" + receiver.slice(2).padStart(64, "0")) as HexString;
164
+
165
+ return Object.freeze({
166
+ receiver: receiverEncoded,
167
+ data: "0x" as HexString,
168
+ tokenAmounts: Object.freeze([Object.freeze({ token, amount })]),
169
+ feeToken: "0x0000000000000000000000000000000000000000" as Address,
170
+ extraArgs: "0x" as HexString,
171
+ });
172
+ };
@@ -0,0 +1,19 @@
1
+ export { default as CCIPRouterAbi } from "./ccip.router.abi";
2
+ export {
3
+ // Chain selectors
4
+ CCIP_CHAIN_SELECTORS,
5
+ getCCIPChainSelector,
6
+ // getFee
7
+ CCIPGetFeeCalldata,
8
+ CCIPGetFeeTrx,
9
+ // ccipSend
10
+ CCIPSendCalldata,
11
+ ccipSendTrx,
12
+ // Helper
13
+ buildCCIPMessage,
14
+ // Types
15
+ type CCIPTokenAmount,
16
+ type EVM2AnyMessage,
17
+ type GetFeeArgs,
18
+ type CCIPSendArgs,
19
+ } from "./ccip.router";
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Enso Shortcuts API client.
3
+ *
4
+ * Fetches a swap route from Enso's public `/v1/shortcuts/route` endpoint and
5
+ * returns the raw `tx.data` (which is always `routeSingle` calldata — see
6
+ * `enso.ts` for the rewrap into `safeRouteSingle`).
7
+ *
8
+ * API reference: https://docs.enso.build/pages/api-reference/shortcuts/route
9
+ */
10
+
11
+ import type { Address } from "viem";
12
+ import type { HexString } from "../../types";
13
+
14
+ export const ENSO_API_BASE = "https://api.enso.finance" as const;
15
+
16
+ /**
17
+ * Raw response shape from the Enso route endpoint. We only model the fields
18
+ * we use; Enso returns many more (amountOut, priceImpact, route, …) that are
19
+ * intentionally ignored.
20
+ */
21
+ export type EnsoRouteResponse = Readonly<{
22
+ tx: Readonly<{
23
+ to: HexString;
24
+ data: HexString;
25
+ value: string;
26
+ }>;
27
+ }>;
28
+
29
+ export type EnsoRouteArgs = Readonly<{
30
+ chainId: number;
31
+ /** Caller address — also used as `receiver` by default. */
32
+ fromAddress: Address;
33
+ /** Destination for the swap output. Typically equal to `fromAddress` (the Safe). */
34
+ receiver: Address;
35
+ tokenIn: Address;
36
+ tokenOut: Address;
37
+ /** Decimal string, no decimals adjustment — matches token units. */
38
+ amountIn: string;
39
+ /**
40
+ * Slippage in integer basis points (e.g. 20 = 0.2%, 300 = 3%).
41
+ *
42
+ * Enso's `slippage` param is a number string in bips. Passing a decimal
43
+ * like `0.2` triggers a 400 Bad Request ("slippage must be a number
44
+ * string").
45
+ */
46
+ slippageBips: number;
47
+ /** Optional Enso API key for 10 rps; no key = 1 rps global limit. */
48
+ apiKey?: string;
49
+ /** Override the API base URL (useful for tests). */
50
+ apiBase?: string;
51
+ }>;
52
+
53
+ export class EnsoRouteApiError extends Error {
54
+ readonly status: number;
55
+ readonly statusText: string;
56
+ readonly tokenIn: Address;
57
+ readonly tokenOut: Address;
58
+
59
+ constructor({
60
+ status,
61
+ statusText,
62
+ tokenIn,
63
+ tokenOut,
64
+ }: {
65
+ status: number;
66
+ statusText: string;
67
+ tokenIn: Address;
68
+ tokenOut: Address;
69
+ }) {
70
+ super(`Enso route API request failed with status ${status} (${statusText}) for swap ${tokenIn} -> ${tokenOut}`);
71
+ this.name = "EnsoRouteApiError";
72
+ this.status = status;
73
+ this.statusText = statusText;
74
+ this.tokenIn = tokenIn;
75
+ this.tokenOut = tokenOut;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Fetch a shortcut route from Enso. Always returns `routeSingle` calldata —
81
+ * use `rewrapRouteSingleAsSafeRouteSingle` in `enso.ts` to convert to
82
+ * `safeRouteSingle` before submitting on-chain if your Safe's Zodiac scope
83
+ * only permits safeRouteSingle.
84
+ */
85
+ export const fetchEnsoRoute = async (args: EnsoRouteArgs): Promise<EnsoRouteResponse> => {
86
+ const {
87
+ chainId,
88
+ fromAddress,
89
+ receiver,
90
+ tokenIn,
91
+ tokenOut,
92
+ amountIn,
93
+ slippageBips,
94
+ apiKey,
95
+ apiBase = ENSO_API_BASE,
96
+ } = args;
97
+
98
+ const params = new URLSearchParams({
99
+ chainId: String(chainId),
100
+ fromAddress,
101
+ receiver,
102
+ tokenIn,
103
+ tokenOut,
104
+ amountIn,
105
+ slippage: String(slippageBips),
106
+ });
107
+
108
+ const headers: Record<string, string> = { Accept: "application/json" };
109
+ if (apiKey) {
110
+ headers["Authorization"] = `Bearer ${apiKey}`;
111
+ }
112
+
113
+ const response = await fetch(`${apiBase}/api/v1/shortcuts/route?${params.toString()}`, {
114
+ headers,
115
+ });
116
+
117
+ if (!response.ok) {
118
+ throw new EnsoRouteApiError({
119
+ status: response.status,
120
+ statusText: response.statusText,
121
+ tokenIn,
122
+ tokenOut,
123
+ });
124
+ }
125
+
126
+ return (await response.json()) as EnsoRouteResponse;
127
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Enso RouterV2 — minimal ABI covering the entry points we use:
3
+ *
4
+ * routeSingle(Token tokenOut, bytes data)
5
+ * safeRouteSingle(Token tokenIn, Token tokenOut, address receiver, bytes data)
6
+ *
7
+ * `Token` is Enso's internal struct `{ uint8 tokenType, bytes data }` where:
8
+ * - tokenType = 0 (Native): data = abi.encode(uint256 amount)
9
+ * - tokenType = 1 (ERC20): data = abi.encode(address token, uint256 amount)
10
+ *
11
+ * We intentionally expose only these two selectors — we never call the Enso
12
+ * router with routeMulti / safeRouteMulti / delegate variants from any DAMM
13
+ * context, and shipping their ABIs here would just invite unaudited usage.
14
+ */
15
+
16
+ const tokenTuple = {
17
+ name: "token",
18
+ type: "tuple",
19
+ components: [
20
+ { name: "tokenType", type: "uint8" },
21
+ { name: "data", type: "bytes" },
22
+ ],
23
+ } as const;
24
+
25
+ export default [
26
+ {
27
+ name: "routeSingle",
28
+ type: "function",
29
+ stateMutability: "payable",
30
+ inputs: [
31
+ { ...tokenTuple, name: "tokenOut" },
32
+ { name: "data", type: "bytes" },
33
+ ],
34
+ outputs: [],
35
+ },
36
+ {
37
+ name: "safeRouteSingle",
38
+ type: "function",
39
+ stateMutability: "payable",
40
+ inputs: [
41
+ { ...tokenTuple, name: "tokenIn" },
42
+ { ...tokenTuple, name: "tokenOut" },
43
+ { name: "receiver", type: "address" },
44
+ { name: "data", type: "bytes" },
45
+ ],
46
+ outputs: [],
47
+ },
48
+ ] as const;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Enso RouterV2 transaction builders.
3
+ *
4
+ * Enso's `/v1/shortcuts/route` API only returns `routeSingle(Token tokenOut,
5
+ * bytes data)` calldata. Our Zodiac Roles scopes only permit the stricter
6
+ * 4-arg `safeRouteSingle(Token tokenIn, Token tokenOut, address receiver,
7
+ * bytes data)`, which validates tokenIn consumption and an explicit receiver.
8
+ *
9
+ * The shortcut `bundle` payload is identical between the two entry points —
10
+ * safeRouteSingle just layers additional validation on top — so we fetch the
11
+ * route once, extract the bundle, and re-encode as safeRouteSingle.
12
+ *
13
+ * Workflow:
14
+ * ensoSafeRouteSingleTrx({ api args, routerAddress })
15
+ * → fetchEnsoRoute(...) (HTTP)
16
+ * → rewrapRouteSingleAsSafeRouteSingle(...) (pure)
17
+ * → createCall({ to: routerAddress, data, … }) (Unwrapable<Call>)
18
+ */
19
+
20
+ import { decodeFunctionData, encodeAbiParameters, encodeFunctionData, type Address } from "viem";
21
+ import type { Call, HexString, Unwrapable } from "../../types";
22
+ import { createCall } from "../../types";
23
+ import ensoRouterAbi from "./enso.router.abi.ts";
24
+ import { fetchEnsoRoute, type EnsoRouteArgs, type EnsoRouteResponse } from "./enso.route.api.ts";
25
+
26
+ // --- Token struct --------------------------------------------------------
27
+
28
+ export enum EnsoTokenType {
29
+ Native = 0,
30
+ ERC20 = 1,
31
+ }
32
+
33
+ export type EnsoToken = Readonly<{
34
+ tokenType: EnsoTokenType;
35
+ data: HexString;
36
+ }>;
37
+
38
+ const ZERO_ADDRESS: Address = "0x0000000000000000000000000000000000000000";
39
+
40
+ const isNativeToken = (token: Address): boolean => token.toLowerCase() === ZERO_ADDRESS;
41
+
42
+ /**
43
+ * Encode a token as Enso's `Token` struct:
44
+ * - ERC20: (tokenType=1, abi.encode(address, uint256))
45
+ * - Native: (tokenType=0, abi.encode(uint256))
46
+ */
47
+ export const encodeEnsoToken = (token: Address, amount: bigint): EnsoToken => {
48
+ if (isNativeToken(token)) {
49
+ return {
50
+ tokenType: EnsoTokenType.Native,
51
+ data: encodeAbiParameters([{ type: "uint256" }], [amount]),
52
+ };
53
+ }
54
+ return {
55
+ tokenType: EnsoTokenType.ERC20,
56
+ data: encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [token, amount]),
57
+ };
58
+ };
59
+
60
+ // --- routeSingle → safeRouteSingle rewrap --------------------------------
61
+
62
+ const ROUTE_SINGLE_SELECTOR = "0xb94c3609" as const;
63
+
64
+ export class UnexpectedEnsoSelectorError extends Error {
65
+ readonly selector: string;
66
+ constructor(selector: string) {
67
+ super(
68
+ `Expected routeSingle selector ${ROUTE_SINGLE_SELECTOR}, got ${selector}. ` +
69
+ `The Enso API may have changed its output format.`,
70
+ );
71
+ this.name = "UnexpectedEnsoSelectorError";
72
+ this.selector = selector;
73
+ }
74
+ }
75
+
76
+ export type RewrapArgs = Readonly<{
77
+ routeSingleCalldata: HexString;
78
+ tokenIn: Address;
79
+ amountIn: bigint;
80
+ receiver: Address;
81
+ }>;
82
+
83
+ /**
84
+ * Decode `routeSingle(tokenOut, bundle)` calldata, then re-encode as
85
+ * `safeRouteSingle(tokenIn, tokenOut, receiver, bundle)` using the same
86
+ * bundle payload. The bundle is byte-identical; only the wrapper changes.
87
+ *
88
+ * Throws `UnexpectedEnsoSelectorError` if the input doesn't start with the
89
+ * routeSingle selector — a fail-fast guard in case Enso's API ever switches
90
+ * output format.
91
+ */
92
+ export const rewrapRouteSingleAsSafeRouteSingle = (args: RewrapArgs): HexString => {
93
+ const { routeSingleCalldata, tokenIn, amountIn, receiver } = args;
94
+
95
+ const selector = routeSingleCalldata.slice(0, 10).toLowerCase();
96
+ if (selector !== ROUTE_SINGLE_SELECTOR) {
97
+ throw new UnexpectedEnsoSelectorError(selector);
98
+ }
99
+
100
+ const decoded = decodeFunctionData({
101
+ abi: ensoRouterAbi,
102
+ data: routeSingleCalldata,
103
+ });
104
+
105
+ if (decoded.functionName !== "routeSingle") {
106
+ throw new UnexpectedEnsoSelectorError(selector);
107
+ }
108
+ const [tokenOutStruct, bundle] = decoded.args as readonly [EnsoToken, HexString];
109
+
110
+ const tokenInStruct = encodeEnsoToken(tokenIn, amountIn);
111
+
112
+ return encodeFunctionData({
113
+ abi: ensoRouterAbi,
114
+ functionName: "safeRouteSingle",
115
+ args: [tokenInStruct, tokenOutStruct, receiver, bundle],
116
+ }) as HexString;
117
+ };
118
+
119
+ // --- High-level helper: fetch route + rewrap + build Call ----------------
120
+
121
+ export class EnsoUnexpectedRouterError extends Error {
122
+ readonly got: Address;
123
+ readonly expected: Address;
124
+ constructor(got: Address, expected: Address) {
125
+ super(
126
+ `Enso API routed to ${got} but we expected RouterV2 ${expected}. ` +
127
+ `Unsupported routing (e.g. EnsoWallet / ShortcutsDelegate) — aborting.`,
128
+ );
129
+ this.name = "EnsoUnexpectedRouterError";
130
+ this.got = got;
131
+ this.expected = expected;
132
+ }
133
+ }
134
+
135
+ export type EnsoSafeRouteSingleTrxArgs = EnsoRouteArgs &
136
+ Readonly<{
137
+ /** Address of the Enso RouterV2 on the target chain. Must match the scope in Zodiac Roles. */
138
+ routerAddress: Address;
139
+ }>;
140
+
141
+ /**
142
+ * Fetch an Enso route and return a `safeRouteSingle` call ready to be added
143
+ * to a MultisendBuilder.
144
+ *
145
+ * Aborts (throws) if Enso's API returns a tx targeting any address other than
146
+ * the provided RouterV2 — this protects against the API silently switching to
147
+ * EnsoWallet or EnsoShortcutsDelegate, neither of which our Zodiac scope
148
+ * permits.
149
+ */
150
+ export const ensoSafeRouteSingleTrx = async (args: EnsoSafeRouteSingleTrxArgs): Promise<Unwrapable<Call>> => {
151
+ const { routerAddress, tokenIn, amountIn, receiver } = args;
152
+
153
+ const route: EnsoRouteResponse = await fetchEnsoRoute(args);
154
+
155
+ if (route.tx.to.toLowerCase() !== routerAddress.toLowerCase()) {
156
+ throw new EnsoUnexpectedRouterError(route.tx.to as Address, routerAddress);
157
+ }
158
+
159
+ const safeRouteSingleCalldata = rewrapRouteSingleAsSafeRouteSingle({
160
+ routeSingleCalldata: route.tx.data,
161
+ tokenIn,
162
+ amountIn: BigInt(amountIn),
163
+ receiver,
164
+ });
165
+
166
+ return createCall({
167
+ to: routerAddress,
168
+ data: safeRouteSingleCalldata,
169
+ operation: 0,
170
+ value: BigInt(route.tx.value || "0"),
171
+ });
172
+ };
@@ -0,0 +1,3 @@
1
+ export { default as ensoRouterAbi } from "./enso.router.abi";
2
+ export * from "./enso";
3
+ export * from "./enso.route.api";
@@ -13,6 +13,7 @@ export * from "./slipstream";
13
13
  export * from "./morphoVault";
14
14
  export * from "./merkl";
15
15
  export * from "./oft";
16
+ export * from "./ccip";
16
17
  export * from "./cctp";
17
18
  export * from "./erc721";
18
19
  export * from "./iporFusion";
@@ -23,3 +24,4 @@ export * from "./fluidLite";
23
24
  export * from "./weth";
24
25
  export * from "./pendle";
25
26
  export * from "./wormhole";
27
+ export * from "./enso";