@pear-protocol/exchanges-sdk 0.18.0 → 0.19.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 +46 -3
- package/dist/helper/hyperliquid/helper.js +28 -6
- package/dist/index.d.ts +4 -0
- package/dist/syncer/binance/syncer.js +2 -1
- package/dist/syncer/bybit/syncer.js +5 -1
- package/dist/syncer/lighter/syncer.js +9 -5
- package/dist/syncer/okx/syncer.js +2 -1
- package/dist/syncer/trade-fee.js +21 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -413,6 +413,49 @@ await tracker.disconnect(); // closes the WebSocket opened by start()
|
|
|
413
413
|
|
|
414
414
|
Credentials and the trade account are fetched automatically on `connect`; Hyperliquid and Lighter account identifiers come from `connection.account.exchangeIdentifier`.
|
|
415
415
|
|
|
416
|
-
Pear
|
|
417
|
-
|
|
418
|
-
|
|
416
|
+
Pear exposes public trader reads for active accounts without separate sharing or copying
|
|
417
|
+
settings. The default leaderboard requires a linked X account; hiding X omits the identity
|
|
418
|
+
without removing the rank. Exchange public APIs remain address-based.
|
|
419
|
+
|
|
420
|
+
## Hyperliquid subaccount funding
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
import { createHyperliquidHelper } from '@pear-protocol/exchanges-sdk';
|
|
424
|
+
|
|
425
|
+
const helper = createHyperliquidHelper();
|
|
426
|
+
await helper.transferSubAccount(ownerWallet, {
|
|
427
|
+
subAccountAddress: '0x...',
|
|
428
|
+
direction: 'main-to-subaccount', // use 'subaccount-to-main' to return USDC
|
|
429
|
+
amount: '1.25',
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Supply the owning main account's wallet (a viem wallet client/account or an ethers signer) in both
|
|
434
|
+
directions. Every transfer uses owner-signed `sendAsset` with `sourceDex: "spot"` and
|
|
435
|
+
`destinationDex: "spot"`. An approved API agent cannot sign this action. Browser wallet clients use
|
|
436
|
+
their connected chain for EIP-712 signing.
|
|
437
|
+
|
|
438
|
+
For standard accounts, this moves the separate spot USDC balance and does not fund perpetual
|
|
439
|
+
collateral. Unified and portfolio-margin accounts use their shared spot balance. The helper checks
|
|
440
|
+
subaccount ownership before signing. It does not read or change either account's mode.
|
|
441
|
+
|
|
442
|
+
A newly created standard subaccount receives USDC in spot and cannot use that balance as perpetual
|
|
443
|
+
margin. Before trading perps, make a separate owner-signed
|
|
444
|
+
[`usdClassTransfer`](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#transfer-from-spot-account-to-perp-account-and-vice-versa)
|
|
445
|
+
with `toPerp: true` and `subaccount:<address>` appended to the amount. The funding helper does not
|
|
446
|
+
submit this second action. A standard main account with USDC in perps must first move the intended
|
|
447
|
+
amount to spot before calling this helper.
|
|
448
|
+
|
|
449
|
+
The helper uses a fixed USDC token ID for the selected network without a metadata lookup, and the
|
|
450
|
+
amount stays an exact decimal string. Each successful transfer makes two information reads: the
|
|
451
|
+
signer's role and the subaccount's role and ownership. It then submits one transfer action.
|
|
452
|
+
|
|
453
|
+
Amounts must be positive decimal strings with at most six fractional digits and must fit the
|
|
454
|
+
existing safe integer micro-USDC limit (`9007199254.740991` USDC). The helper never rounds an amount.
|
|
455
|
+
|
|
456
|
+
Failures reach the caller without retries or fallback actions. After a timeout or connection loss,
|
|
457
|
+
check transfer history and balances before making another call; submission may have succeeded.
|
|
458
|
+
|
|
459
|
+
The routing and signing follow Hyperliquid's [Send Asset API](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#send-asset),
|
|
460
|
+
[account modes](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/account-abstraction-modes),
|
|
461
|
+
and [official signing implementation](https://github.com/hyperliquid-dex/hyperliquid-python-sdk/blob/master/hyperliquid/utils/signing.py).
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { HttpTransport } from '@nktkas/hyperliquid';
|
|
2
|
-
import {
|
|
2
|
+
import { sendAsset, createSubAccount } from '@nktkas/hyperliquid/api/exchange';
|
|
3
|
+
import { getWalletAddress } from '@nktkas/hyperliquid/signing';
|
|
4
|
+
import { formatUnits } from 'viem';
|
|
3
5
|
import { fetchInfo, fetchUserRole } from '../../client/hyperliquid/rest';
|
|
4
6
|
import { parseCollateralAmount } from '../subaccount.helper';
|
|
5
7
|
|
|
6
8
|
var __defProp = Object.defineProperty;
|
|
7
9
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
10
|
+
const USDC_TOKENS = {
|
|
11
|
+
mainnet: "USDC:0x6d1e7cde53ba9467b783cb7c530ce054",
|
|
12
|
+
testnet: "USDC:0xeb62eee3685fc4c43992febcd9e75443"
|
|
13
|
+
};
|
|
8
14
|
function createHyperliquidHelper(demo = false) {
|
|
9
15
|
return {
|
|
10
16
|
/**
|
|
@@ -33,19 +39,35 @@ function createHyperliquidHelper(demo = false) {
|
|
|
33
39
|
subAccountAddress: response.response.data
|
|
34
40
|
};
|
|
35
41
|
},
|
|
36
|
-
/** Moves
|
|
42
|
+
/** Moves spot USDC between owned accounts using the owning main wallet. */
|
|
37
43
|
async transferSubAccount(wallet, params) {
|
|
38
44
|
const usd = parseCollateralAmount(params.amount, "USDC");
|
|
45
|
+
const ownerAddress = await getWalletAddress(wallet);
|
|
46
|
+
const signerRole = await fetchUserRole(ownerAddress, demo);
|
|
47
|
+
if (signerRole.role === "agent") {
|
|
48
|
+
throw new Error("Spot subaccount transfers require the owner wallet; an approved agent cannot sign");
|
|
49
|
+
}
|
|
50
|
+
if (signerRole.role !== "user") {
|
|
51
|
+
throw new Error("Spot subaccount transfers require the owner wallet");
|
|
52
|
+
}
|
|
53
|
+
const subAccountRole = await fetchUserRole(params.subAccountAddress, demo);
|
|
54
|
+
if (subAccountRole.role !== "subAccount" || subAccountRole.data.master.toLowerCase() !== ownerAddress.toLowerCase()) {
|
|
55
|
+
throw new Error("Subaccount does not belong to the signing main account");
|
|
56
|
+
}
|
|
57
|
+
const isDeposit = isMainToSubAccount(params.direction);
|
|
39
58
|
const transport = new HttpTransport({
|
|
40
59
|
isTestnet: demo
|
|
41
60
|
});
|
|
42
|
-
await
|
|
61
|
+
await sendAsset({
|
|
43
62
|
transport,
|
|
44
63
|
wallet
|
|
45
64
|
}, {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
destination: isDeposit ? params.subAccountAddress : ownerAddress,
|
|
66
|
+
sourceDex: "spot",
|
|
67
|
+
destinationDex: "spot",
|
|
68
|
+
token: demo ? USDC_TOKENS.testnet : USDC_TOKENS.mainnet,
|
|
69
|
+
amount: formatUnits(BigInt(usd), 6),
|
|
70
|
+
fromSubAccount: isDeposit ? "" : params.subAccountAddress
|
|
49
71
|
});
|
|
50
72
|
},
|
|
51
73
|
/** Lists a main account's existing subaccounts for transfer-destination selection. */
|
package/dist/index.d.ts
CHANGED
|
@@ -429,6 +429,7 @@ type SubAccountTransferDirection = 'main-to-subaccount' | 'subaccount-to-main';
|
|
|
429
429
|
interface HyperliquidHelper {
|
|
430
430
|
checkAccountActivation(userAddress: `0x${string}`): Promise<boolean>;
|
|
431
431
|
createSubAccount(wallet: AbstractWallet, params: CreateHyperliquidSubAccountParams): Promise<CreateHyperliquidSubAccountResult>;
|
|
432
|
+
/** Transfers spot USDC in either direction; requires the owning main wallet, never an agent. */
|
|
432
433
|
transferSubAccount(wallet: AbstractWallet, params: HyperliquidSubAccountTransferParams): Promise<void>;
|
|
433
434
|
listSubAccounts(userAddress: `0x${string}`): Promise<HyperliquidSubAccountSummary[]>;
|
|
434
435
|
checkApiWallet(params: CheckApiWalletParams): Promise<boolean>;
|
|
@@ -1448,10 +1449,13 @@ declare class LighterAccountSyncer extends BaseAccountSyncer<RawFill$1> {
|
|
|
1448
1449
|
protected listExternalFunding({ since, instruments, }: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
1449
1450
|
protected isReduceOnly(rawFill: RawFill$1): boolean;
|
|
1450
1451
|
private getAccountSide;
|
|
1452
|
+
/** The client order id on our side of the fill, or null when Lighter reports none. */
|
|
1451
1453
|
private getClientOrderId;
|
|
1452
1454
|
private isMaker;
|
|
1453
1455
|
private getPositionSizeBefore;
|
|
1456
|
+
/** Signed like the rate: positive is a cost, a negative rate a maker rebate (#748). */
|
|
1454
1457
|
private getTradeFee;
|
|
1458
|
+
/** The integrator's cut, which the wire carries unsigned. */
|
|
1455
1459
|
private getIntegratorFee;
|
|
1456
1460
|
/** Prevents testnet reads from silently minting a mainnet auth token. */
|
|
1457
1461
|
private assertAuthenticatedSyncAvailable;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
import { fetchAllPositionRisk, fetchUserTradesSince, fetchAllOrdersSince } from '../../client/binance/rest';
|
|
3
3
|
import { BaseAccountSyncer } from '../base';
|
|
4
|
+
import { toSignedTradeFee } from '../trade-fee';
|
|
4
5
|
|
|
5
6
|
var __defProp = Object.defineProperty;
|
|
6
7
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -66,7 +67,7 @@ class BinanceAccountSyncer extends BaseAccountSyncer {
|
|
|
66
67
|
side: rawFill.side,
|
|
67
68
|
reduceOnly: this.isReduceOnly(rawFill),
|
|
68
69
|
timestamp: rawFill.time,
|
|
69
|
-
tradeFee:
|
|
70
|
+
tradeFee: toSignedTradeFee("binance", rawFill.commission),
|
|
70
71
|
builderFee: "0",
|
|
71
72
|
liquidation: null
|
|
72
73
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
import { fetchPositions, fetchExecutionsSince } from '../../client/bybit/rest';
|
|
3
3
|
import { BaseAccountSyncer } from '../base';
|
|
4
|
+
import { toSignedTradeFee } from '../trade-fee';
|
|
4
5
|
|
|
5
6
|
var __defProp = Object.defineProperty;
|
|
6
7
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -36,6 +37,9 @@ class BybitAccountSyncer extends BaseAccountSyncer {
|
|
|
36
37
|
async listExternalFills({ since }) {
|
|
37
38
|
const executions = await fetchExecutionsSince(this.creds, since, this.demo);
|
|
38
39
|
return executions.reduce((fills, rawFill) => {
|
|
40
|
+
if (rawFill.execType === "Funding") {
|
|
41
|
+
return fills;
|
|
42
|
+
}
|
|
39
43
|
fills.push({
|
|
40
44
|
connector: "bybit",
|
|
41
45
|
exchangeFillId: rawFill.execId,
|
|
@@ -46,7 +50,7 @@ class BybitAccountSyncer extends BaseAccountSyncer {
|
|
|
46
50
|
side: rawFill.side === "Buy" ? "BUY" : "SELL",
|
|
47
51
|
reduceOnly: this.isReduceOnly(rawFill),
|
|
48
52
|
timestamp: Number(rawFill.execTime),
|
|
49
|
-
tradeFee:
|
|
53
|
+
tradeFee: toSignedTradeFee("bybit", rawFill.execFee),
|
|
50
54
|
builderFee: "0",
|
|
51
55
|
liquidation: null
|
|
52
56
|
});
|
|
@@ -119,8 +119,10 @@ class LighterAccountSyncer extends BaseAccountSyncer {
|
|
|
119
119
|
if (rawFill.bid_account_id === this.accountIndex) return "bid";
|
|
120
120
|
return null;
|
|
121
121
|
}
|
|
122
|
+
/** The client order id on our side of the fill, or null when Lighter reports none. */
|
|
122
123
|
getClientOrderId(rawFill, side) {
|
|
123
|
-
|
|
124
|
+
const clientOrderId = side === "ask" ? rawFill.ask_client_id_str : rawFill.bid_client_id_str;
|
|
125
|
+
return clientOrderId && clientOrderId !== "0" ? clientOrderId : null;
|
|
124
126
|
}
|
|
125
127
|
isMaker(rawFill, side) {
|
|
126
128
|
return side === "ask" ? rawFill.is_maker_ask : !rawFill.is_maker_ask;
|
|
@@ -129,13 +131,15 @@ class LighterAccountSyncer extends BaseAccountSyncer {
|
|
|
129
131
|
const value = this.isMaker(rawFill, side) ? rawFill.maker_position_size_before : rawFill.taker_position_size_before;
|
|
130
132
|
return new BigNumber(value ?? 0);
|
|
131
133
|
}
|
|
134
|
+
/** Signed like the rate: positive is a cost, a negative rate a maker rebate (#748). */
|
|
132
135
|
getTradeFee(rawFill, side) {
|
|
133
136
|
const rateTick = this.isMaker(rawFill, side) ? rawFill.maker_fee : rawFill.taker_fee;
|
|
134
|
-
return this.feeTickToAmount(rateTick, rawFill.usd_amount);
|
|
137
|
+
return this.feeTickToAmount(rateTick, rawFill.usd_amount).toFixed();
|
|
135
138
|
}
|
|
139
|
+
/** The integrator's cut, which the wire carries unsigned. */
|
|
136
140
|
getIntegratorFee(rawFill, side) {
|
|
137
141
|
const rateTick = this.isMaker(rawFill, side) ? rawFill.integrator_maker_fee : rawFill.integrator_taker_fee;
|
|
138
|
-
return this.feeTickToAmount(rateTick, rawFill.usd_amount);
|
|
142
|
+
return this.feeTickToAmount(rateTick, rawFill.usd_amount).abs().toFixed();
|
|
139
143
|
}
|
|
140
144
|
/** Prevents testnet reads from silently minting a mainnet auth token. */
|
|
141
145
|
assertAuthenticatedSyncAvailable() {
|
|
@@ -145,8 +149,8 @@ class LighterAccountSyncer extends BaseAccountSyncer {
|
|
|
145
149
|
}
|
|
146
150
|
// Lighter fees come as ppm rates (÷1e6), not amounts — the charged fee is that rate times the trade's USD notional.
|
|
147
151
|
feeTickToAmount(rateTick, usdAmount) {
|
|
148
|
-
const rate = new BigNumber(rateTick ?? 0).
|
|
149
|
-
return rate.times(usdAmount)
|
|
152
|
+
const rate = new BigNumber(rateTick ?? 0).div(LIGHTER_FEE_TICK_DENOMINATOR);
|
|
153
|
+
return rate.times(usdAmount);
|
|
150
154
|
}
|
|
151
155
|
}
|
|
152
156
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
import { fetchPositions, fetchFillsHistorySince } from '../../client/okx/rest';
|
|
3
3
|
import { BaseAccountSyncer } from '../base';
|
|
4
|
+
import { toSignedTradeFee } from '../trade-fee';
|
|
4
5
|
|
|
5
6
|
var __defProp = Object.defineProperty;
|
|
6
7
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -51,7 +52,7 @@ class OkxAccountSyncer extends BaseAccountSyncer {
|
|
|
51
52
|
side: rawFill.side === "buy" ? "BUY" : "SELL",
|
|
52
53
|
reduceOnly: this.isReduceOnly(rawFill),
|
|
53
54
|
timestamp: Number(rawFill.fillTime || rawFill.ts),
|
|
54
|
-
tradeFee:
|
|
55
|
+
tradeFee: toSignedTradeFee("okx", rawFill.fee),
|
|
55
56
|
builderFee: "0",
|
|
56
57
|
liquidation: null
|
|
57
58
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parse, negate } from '@pear-protocol/utils';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
5
|
+
function toSignedTradeFee(connector, rawFee) {
|
|
6
|
+
const fee = parse(rawFee || "0");
|
|
7
|
+
switch (connector) {
|
|
8
|
+
// Binance `userTrades.commission`: a charge is positive. The docs do not state a rebate's sign; it is
|
|
9
|
+
// taken to be negative, the same convention as ours.
|
|
10
|
+
case "binance":
|
|
11
|
+
// Bybit `execution/list.execFee`: positive is an expense, negative a rebate (v5 transaction-log docs).
|
|
12
|
+
case "bybit":
|
|
13
|
+
return fee.toFixed();
|
|
14
|
+
// OKX `fills-history.fee`: a deduction is negative, a rebate positive (v5 docs), the opposite of ours.
|
|
15
|
+
case "okx":
|
|
16
|
+
return negate(fee).toFixed();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
__name(toSignedTradeFee, "toSignedTradeFee");
|
|
20
|
+
|
|
21
|
+
export { toSignedTradeFee };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pear-protocol/exchanges-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Pear Protocol Exchanges SDK",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@nktkas/hyperliquid": "0.33.2",
|
|
31
|
-
"@pear-protocol/core-sdk": "^2.
|
|
32
|
-
"@pear-protocol/types": "^2.
|
|
33
|
-
"@pear-protocol/utils": "^1.
|
|
31
|
+
"@pear-protocol/core-sdk": "^2.12.0",
|
|
32
|
+
"@pear-protocol/types": "^2.19.0",
|
|
33
|
+
"@pear-protocol/utils": "^1.4.0",
|
|
34
34
|
"bignumber.js": "^11.1.1",
|
|
35
35
|
"partysocket": "^1.0.3",
|
|
36
36
|
"viem": "^2.48.8",
|