@dhedge/v2-sdk 2.2.0 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhedge/v2-sdk",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "license": "MIT",
5
5
  "description": "🛠 An SDK for building applications on top of dHEDGE V2",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,30 @@
1
+ [
2
+ {
3
+ "inputs": [
4
+ { "name": "tokenIn", "type": "address" },
5
+ { "name": "amountIn", "type": "uint256" },
6
+ { "name": "tokenOut", "type": "address" },
7
+ { "name": "amountOutMinimum", "type": "uint256" },
8
+ { "name": "attestationSignature", "type": "bytes" },
9
+ {
10
+ "components": [
11
+ { "name": "chainId", "type": "uint256" },
12
+ { "name": "attestationId", "type": "uint256" },
13
+ { "name": "userId", "type": "bytes32" },
14
+ { "name": "asset", "type": "address" },
15
+ { "name": "price", "type": "uint256" },
16
+ { "name": "quantity", "type": "uint256" },
17
+ { "name": "expiration", "type": "uint256" },
18
+ { "name": "side", "type": "uint8" },
19
+ { "name": "additionalData", "type": "bytes32" }
20
+ ],
21
+ "name": "quote",
22
+ "type": "tuple"
23
+ }
24
+ ],
25
+ "name": "swapExactInWithAttestation",
26
+ "outputs": [],
27
+ "stateMutability": "nonpayable",
28
+ "type": "function"
29
+ }
30
+ ]
package/src/config.ts CHANGED
@@ -77,7 +77,8 @@ export const routerAddress: AddressDappNetworkMap = {
77
77
  [Dapp.ODOS]: "0x0D05a7D3448512B78fa8A9e46c4872C88C4a0D05",
78
78
  [Dapp.PENDLE]: "0x888888888889758F76e7103c6CbF23ABbF58F946",
79
79
  [Dapp.ONEINCH]: "0x111111125421ca6dc452d289314280a0f8842a65",
80
- [Dapp.KYBERSWAP]: "0x6131B5fae19EA4f9D964eAc0408E4408b66337b5"
80
+ [Dapp.KYBERSWAP]: "0x6131B5fae19EA4f9D964eAc0408E4408b66337b5",
81
+ [Dapp.ONDO]: "0xde41399145F23936b03dD1474eC16c1519c0DC2a"
81
82
  },
82
83
  [Network.PLASMA]: {
83
84
  [Dapp.AAVEV3]: "0x925a2A7214Ed92428B5b1B090F80b25700095e12",
@@ -87,6 +87,7 @@ import {
87
87
  } from "../services/toros/limitOrder";
88
88
  import { getKyberSwapTxData } from "../services/kyberSwap";
89
89
  import { getCowSwapTxData } from "../services/cowSwap";
90
+ import { getOndoSwapTxData } from "../services/ondo";
90
91
  import {
91
92
  getClosePositionHyperliquidTxData,
92
93
  getDepositHyperliquidTxData,
@@ -451,6 +452,15 @@ export class Pool {
451
452
  slippage
452
453
  ));
453
454
  break;
455
+ case Dapp.ONDO:
456
+ ({ swapTxData, minAmountOut } = await getOndoSwapTxData(
457
+ this,
458
+ assetFrom,
459
+ assetTo,
460
+ amountIn.toString(),
461
+ slippage
462
+ ));
463
+ break;
454
464
  case Dapp.COWSWAP: {
455
465
  const cowSwapEstimateGas = isSdkOptionsBoolean(sdkOptions)
456
466
  ? sdkOptions
@@ -0,0 +1,142 @@
1
+ import axios from "axios";
2
+ import { ethers } from "ethers";
3
+ import BigNumber from "bignumber.js";
4
+ import { ApiError, Pool } from "../..";
5
+ import IOndoGMSwap from "../../abi/ondo/IOndoGMSwap.json";
6
+
7
+ const ONDO_API_URL = "https://api.gm.ondo.finance/v1/attestations";
8
+
9
+ // Ethereum mainnet USDC — Ondo is Ethereum-only
10
+ const USDC_ETHEREUM = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
11
+
12
+ type OndoAttestation = {
13
+ attestationId: string;
14
+ userId: string;
15
+ chainId: string;
16
+ assetAddress: string;
17
+ side: string;
18
+ tokenAmount: string;
19
+ price: string;
20
+ expiration: number;
21
+ signature: string;
22
+ additionalData: string;
23
+ };
24
+
25
+ const iface = new ethers.utils.Interface(IOndoGMSwap);
26
+
27
+ // Ondo returns userId as a left-aligned 32-byte hex (significant bytes first,
28
+ // e.g. 0x474d...0000), so right-pad to preserve byte order and fix it to 32 bytes.
29
+ function toBytes32(s: string): string {
30
+ const hex = s.startsWith("0x") ? s.slice(2) : s;
31
+ return "0x" + hex.padEnd(64, "0").slice(0, 64);
32
+ }
33
+
34
+ async function postOndoAttestation(
35
+ symbol: string,
36
+ side: "buy" | "sell",
37
+ amount: { notionalValue: string } | { tokenAmount: string },
38
+ apiKey: string
39
+ ): Promise<OndoAttestation> {
40
+ try {
41
+ const { data } = await axios.post(
42
+ ONDO_API_URL,
43
+ { chainId: "ethereum-1", symbol, side, ...amount, duration: "short" },
44
+ { headers: { "x-api-key": apiKey } }
45
+ );
46
+ return data as OndoAttestation;
47
+ } catch (error) {
48
+ // Surface Ondo's structured error (e.g. ASSET_NOT_FOUND, MARKET_CLOSED)
49
+ if (axios.isAxiosError(error) && error.response) {
50
+ const { code, message } = error.response.data ?? {};
51
+ throw new ApiError(
52
+ `Ondo attestation request failed (${error.response.status}): ${code ??
53
+ ""} ${message ?? JSON.stringify(error.response.data)}`.trim()
54
+ );
55
+ }
56
+ throw new ApiError(
57
+ `Ondo attestation request failed: ${
58
+ error instanceof Error ? error.message : String(error)
59
+ }`
60
+ );
61
+ }
62
+ }
63
+
64
+ export async function getOndoSwapTxData(
65
+ pool: Pool,
66
+ tokenIn: string,
67
+ tokenOut: string,
68
+ amountIn: string,
69
+ slippage: number
70
+ ): Promise<{ swapTxData: string; minAmountOut: string }> {
71
+ const apiKey = process.env.ONDO_API_KEY;
72
+ if (!apiKey) throw new Error("ONDO_API_KEY environment variable is not set");
73
+
74
+ const amount = new BigNumber(amountIn);
75
+ const isMint = tokenIn.toLowerCase() === USDC_ETHEREUM.toLowerCase();
76
+ const gmToken = isMint ? tokenOut : tokenIn;
77
+ const slippageBps = Math.round(slippage * 100);
78
+
79
+ const tokenContract = new ethers.Contract(
80
+ gmToken,
81
+ ["function symbol() view returns (string)"],
82
+ pool.signer
83
+ );
84
+ const symbol: string = await tokenContract.symbol();
85
+
86
+ // For mint: notionalValue is the USD amount to subscribe. USDonManager values
87
+ // USDC at par (1 USDC = 1 USDon), so pass the USDC amount as-is (6dp -> decimal).
88
+ // Discounting by a USDC market price would strand the gap as USDon in the swapper
89
+ // (and revert via ExcessiveAmountIn once it exceeds the 1 USDC tolerance).
90
+ // For redeem: pass the GM token amount directly as tokenAmount.
91
+ const attestationAmount = isMint
92
+ ? { notionalValue: amount.div(1e6).toFixed(18) }
93
+ : { tokenAmount: amount.div(1e18).toFixed(18) };
94
+
95
+ const attestation = await postOndoAttestation(
96
+ symbol,
97
+ isMint ? "buy" : "sell",
98
+ attestationAmount,
99
+ apiKey
100
+ );
101
+
102
+ const signature =
103
+ "0x" + Buffer.from(attestation.signature, "base64").toString("hex");
104
+ const additionalData = ethers.utils.hexZeroPad(
105
+ "0x" + Buffer.from(attestation.additionalData, "base64").toString("hex"),
106
+ 32
107
+ );
108
+ const quantity = new BigNumber(attestation.tokenAmount);
109
+ const priceD18 = new BigNumber(attestation.price);
110
+
111
+ let minAmountOut: BigNumber;
112
+ if (isMint) {
113
+ minAmountOut = quantity.times(10000 - slippageBps).div(10000);
114
+ } else {
115
+ // USDC out (6 dec) = quantity_D18 * price_D18 / 1e30
116
+ const usdcOut = quantity.times(priceD18).div(1e30);
117
+ minAmountOut = usdcOut.times(10000 - slippageBps).div(10000);
118
+ }
119
+
120
+ const quote = {
121
+ chainId: 1,
122
+ attestationId: attestation.attestationId,
123
+ userId: toBytes32(attestation.userId),
124
+ asset: attestation.assetAddress,
125
+ price: priceD18.toFixed(0),
126
+ quantity: quantity.toFixed(0),
127
+ expiration: attestation.expiration,
128
+ side: Number(attestation.side),
129
+ additionalData
130
+ };
131
+
132
+ const swapTxData = iface.encodeFunctionData("swapExactInWithAttestation", [
133
+ tokenIn,
134
+ amount.toFixed(0),
135
+ tokenOut,
136
+ minAmountOut.toFixed(0),
137
+ signature,
138
+ quote
139
+ ]);
140
+
141
+ return { swapTxData, minAmountOut: minAmountOut.toFixed(0) };
142
+ }
@@ -0,0 +1,132 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+
3
+ /**
4
+ * Ondo Global Markets on-chain tests for minting and redeeming GM tokens.
5
+ * These require a live chain connection (onFork: false) because each mint/redeem
6
+ * needs a fresh attestation signed by the Ondo API against current chain state,
7
+ * which cannot be reproduced on a Hardhat fork.
8
+ *
9
+ * Prerequisites:
10
+ * - PRIVATE_KEY in .env (must be the pool manager or trader)
11
+ * - ETHEREUM_URL in .env
12
+ * - ONDO_API_KEY in .env
13
+ * - The test pool must hold USDC to mint (and SPYon, from the mint, to redeem)
14
+ */
15
+
16
+ import { ethers } from "ethers";
17
+ import { Dhedge, Pool } from "..";
18
+
19
+ import { Dapp, Network } from "../types";
20
+ import { CONTRACT_ADDRESS, MAX_AMOUNT } from "./constants";
21
+ import { TestingRunParams, testingHelper } from "./utils/testingHelper";
22
+
23
+ import { getTxOptions } from "./txOptions";
24
+ import { balanceDelta } from "./utils/token";
25
+
26
+ import { routerAddress } from "../config";
27
+
28
+ const testOndo = ({ wallet, network }: TestingRunParams) => {
29
+ const USDC = CONTRACT_ADDRESS[network].USDC;
30
+ const SPYon = "0xfedc5f4a6c38211c1338aa411018dfaf26612c08";
31
+
32
+ let dhedge: Dhedge;
33
+ let pool: Pool;
34
+ jest.setTimeout(100000);
35
+
36
+ describe(`pool on ${network}`, () => {
37
+ beforeAll(async () => {
38
+ dhedge = new Dhedge(wallet, network);
39
+ pool = await dhedge.loadPool(
40
+ "0x9f647b85A514b1e60F8E8E956E636a50dA406279"
41
+ );
42
+ });
43
+
44
+ it("approves unlimited USDC on Ondo", async () => {
45
+ const tx = await pool.approve(Dapp.ONDO, USDC, MAX_AMOUNT);
46
+ await tx.wait();
47
+ const iERC20 = new ethers.Contract(
48
+ USDC,
49
+ ["function allowance(address,address) view returns (uint256)"],
50
+ pool.signer
51
+ );
52
+ const allowance = await iERC20.allowance(
53
+ pool.address,
54
+ routerAddress[network][Dapp.ONDO]!
55
+ );
56
+ expect(allowance.gt(0)).toBe(true);
57
+ });
58
+
59
+ it("gets gas estimation for 40 USDC into SPYon on Ondo", async () => {
60
+ const gasEstimate = await pool.trade(
61
+ Dapp.ONDO,
62
+ USDC,
63
+ SPYon,
64
+ "40000000",
65
+ 0.1,
66
+ await getTxOptions(network),
67
+ true
68
+ );
69
+ expect(gasEstimate.gasEstimationError).toBeNull();
70
+ expect(gasEstimate.gas.gt(0)).toBe(true);
71
+ expect(gasEstimate.minAmountOut).not.toBeNull();
72
+ });
73
+
74
+ it("trades 40 USDC into SPYon on Ondo", async () => {
75
+ const tx = await pool.trade(
76
+ Dapp.ONDO,
77
+ USDC,
78
+ SPYon,
79
+ "40000000",
80
+ 0.1,
81
+ await getTxOptions(network)
82
+ );
83
+ await tx.wait();
84
+ const spBalanceDelta = await balanceDelta(
85
+ pool.address,
86
+ SPYon,
87
+ pool.signer
88
+ );
89
+ expect(spBalanceDelta.gt(0)).toBe(true);
90
+ });
91
+
92
+ it("approves unlimited SPYon on Ondo", async () => {
93
+ const tx = await pool.approve(Dapp.ONDO, SPYon, MAX_AMOUNT);
94
+ await tx.wait();
95
+ const iERC20 = new ethers.Contract(
96
+ SPYon,
97
+ ["function allowance(address,address) view returns (uint256)"],
98
+ pool.signer
99
+ );
100
+ const allowance = await iERC20.allowance(
101
+ pool.address,
102
+ routerAddress[network][Dapp.ONDO]!
103
+ );
104
+ expect(allowance.gt(0)).toBe(true);
105
+ });
106
+
107
+ it("sells SPYon balance on Ondo", async () => {
108
+ const spyonBalance = await pool.utils.getBalance(SPYon, pool.address);
109
+ const tx = await pool.trade(
110
+ Dapp.ONDO,
111
+ SPYon,
112
+ USDC,
113
+ spyonBalance,
114
+ 0.1,
115
+ await getTxOptions(network)
116
+ );
117
+ await tx.wait();
118
+ const usdcBalanceDelta = await balanceDelta(
119
+ pool.address,
120
+ USDC,
121
+ pool.signer
122
+ );
123
+ expect(usdcBalanceDelta.gt(0)).toBe(true);
124
+ });
125
+ });
126
+ };
127
+
128
+ testingHelper({
129
+ network: Network.ETHEREUM,
130
+ testingRun: testOndo,
131
+ onFork: false
132
+ });
package/src/types.ts CHANGED
@@ -37,7 +37,8 @@ export enum Dapp {
37
37
  PENDLE = "pendle",
38
38
  KYBERSWAP = "kyberswap",
39
39
  HYPERLIQUID = "hyperliquid",
40
- COWSWAP = "cowswap"
40
+ COWSWAP = "cowswap",
41
+ ONDO = "ondo"
41
42
  }
42
43
 
43
44
  /** Function-name strings used when encoding ABI calls — keep in sync with the