dipcoin-cli 1.3.0 → 1.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 CHANGED
@@ -101,9 +101,12 @@ dipcoin-cli market pairs # List all trading pairs
101
101
  dipcoin-cli market ticker <symbol> # Ticker (price, volume, funding)
102
102
  dipcoin-cli market orderbook <symbol> # Order book
103
103
  dipcoin-cli market oracle <symbol> # Oracle price
104
+ dipcoin-cli market kline <symbol> # Candlestick history (default 1h, last 24h)
105
+ dipcoin-cli market kline BTC --interval 5m --count 20
106
+ dipcoin-cli market kline BTC --interval 1d --from <unix-sec> --to <unix-sec>
104
107
  ```
105
108
 
106
- Symbols auto-normalize: `BTC` becomes `BTC-PERP`.
109
+ Symbols auto-normalize: `BTC` becomes `BTC-PERP`. `kline` intervals: `1m / 5m / 15m / 30m / 1h / 4h / 1d / 1w`.
107
110
 
108
111
  #### account
109
112
 
@@ -145,8 +148,14 @@ dipcoin-cli trade orders --symbol BTC-PERP
145
148
 
146
149
  # Cancel orders
147
150
  dipcoin-cli trade cancel <symbol> <hash1> [hash2...]
151
+
152
+ # Adjust default (ISOLATED) leverage for a market (REST update, no on-chain tx)
153
+ dipcoin-cli trade leverage BTC 10
154
+ dipcoin-cli trade leverage ETH 5
148
155
  ```
149
156
 
157
+ DipCoin perp currently only supports ISOLATED margin; CROSS is not exposed by the backend.
158
+
150
159
  **Trade Options:**
151
160
 
152
161
  | Option | Description |
@@ -172,13 +181,19 @@ dipcoin-cli position tpsl <symbol> --side <buy|sell> --quantity <q> --leverage <
172
181
  dipcoin-cli position tpsl <symbol> --side sell --quantity 0.01 --leverage 10 \
173
182
  --tp-trigger 105000 --tp-type limit --tp-price 105000
174
183
 
175
- # Margin operations (on-chain)
184
+ # Margin operations (on-chain) — auto-prepends a fresh signed price update
176
185
  dipcoin-cli position margin add <symbol> <amount>
177
186
  dipcoin-cli position margin remove <symbol> <amount>
187
+
188
+ # Same, but target a vault's position
189
+ dipcoin-cli position margin add <symbol> <amount> --vault <vaultId>
190
+ dipcoin-cli position margin remove <symbol> <amount> --vault <vaultId>
178
191
  ```
179
192
 
180
193
  The `--side` is the **closing side**: use `sell` for long positions, `buy` for short.
181
194
 
195
+ > All on-chain margin / vault writes (`position margin add/remove`, `vault deposit/close/withdraw/claim/fill-withdrawals`) fetch a fresh signed price from `/api/perp-market-api/price/latest` and bundle a `price_feed::update_price_feed` PTB step before the actual call — required by the v6 oracle freshness check.
196
+
182
197
  #### vault
183
198
 
184
199
  On-chain vault operations (DipCoin vault contracts):
@@ -197,9 +212,9 @@ dipcoin-cli vault position <vaultId> --address <addr> # Query another address
197
212
  dipcoin-cli vault deposit <vaultId> <amount>
198
213
  dipcoin-cli vault withdraw <vaultId> <shares>
199
214
  dipcoin-cli vault withdraw <vaultId> --all # Withdraw all shares
200
- dipcoin-cli vault fill <vaultId> <requestIDs...> [--markets <ids>]
201
- dipcoin-cli vault close <vaultId> [--markets <ids>]
202
- dipcoin-cli vault remove <vaultId>
215
+ dipcoin-cli vault fill-withdrawals <vaultId> <requestIDs...> # creator/operator only
216
+ dipcoin-cli vault close <vaultId>
217
+ dipcoin-cli vault remove <vaultId> # operator-only (normal creators get "unauthorized")
203
218
  dipcoin-cli vault claim <vaultId>
204
219
  dipcoin-cli vault set-trader <vaultId> <address>
205
220
  dipcoin-cli vault set-sub-trader <vaultId> <address> [--disable]
package/dist/cli.cjs CHANGED
@@ -12973,7 +12973,11 @@ function formatNormalToWei(value, decimals = 18) {
12973
12973
  * @returns BigNumber instance
12974
12974
  */
12975
12975
  function formatNormalToWeiBN(value, decimals = 18) {
12976
+ if (!value && value !== 0)
12977
+ return new BigNumber__default["default"](0);
12976
12978
  const bn = new BigNumber__default["default"](value);
12979
+ if (bn.isNaN())
12980
+ return new BigNumber__default["default"](0);
12977
12981
  return bn.multipliedBy(new BigNumber__default["default"](10).pow(decimals));
12978
12982
  }
12979
12983
  /**
@@ -16369,6 +16373,8 @@ const API_ENDPOINTS = {
16369
16373
  FUNDING_SETTLEMENTS: "/api/perp-trade-api/history/funding-settlements",
16370
16374
  BALANCE_CHANGES: "/api/perp-trade-api/history/balance-changes",
16371
16375
  ORACLE: "/api/perp-market-api/oracle",
16376
+ SIGNED_PRICE_LATEST: "/api/perp-market-api/price/latest",
16377
+ KLINE_HISTORY: "/api/perp-market-api/kline/history",
16372
16378
  VAULTS_BY_CREATOR: "/api/perp-vault-api/public/vaults/by-creator",
16373
16379
  VAULTS_PUBLIC: "/api/perp-vault-api/public/vaults",
16374
16380
  // Points & Referral
@@ -18558,6 +18564,77 @@ class DipCoinPerpSDK {
18558
18564
  return { status: false, error: formatError(error) };
18559
18565
  }
18560
18566
  }
18567
+ /**
18568
+ * Fetch kline (candlestick) history for a market.
18569
+ *
18570
+ * @param params.symbol Trading pair (e.g. "BTC-PERP")
18571
+ * @param params.interval Bar interval (e.g. "1m" / "5m" / "1h" / "1d")
18572
+ * @param params.from Inclusive start timestamp (Unix seconds)
18573
+ * @param params.to Exclusive end timestamp (Unix seconds)
18574
+ * @param params.countback Optional cap on returned bars (most recent first)
18575
+ */
18576
+ async getKlineHistory(params) {
18577
+ try {
18578
+ if (!params?.symbol)
18579
+ return { status: false, error: "symbol is required" };
18580
+ if (!params?.interval)
18581
+ return { status: false, error: "interval is required" };
18582
+ // Server requires both startTime and endTime in unix seconds.
18583
+ const nowSec = Math.floor(Date.now() / 1000);
18584
+ const startTime = params.from ?? nowSec - 86400;
18585
+ const endTime = params.to ?? nowSec;
18586
+ const response = await this.httpClient.get(API_ENDPOINTS.KLINE_HISTORY, {
18587
+ params: {
18588
+ symbol: params.symbol,
18589
+ interval: params.interval,
18590
+ startTime,
18591
+ endTime,
18592
+ ...(params.countback !== undefined ? { countback: params.countback } : {}),
18593
+ },
18594
+ });
18595
+ if (response.code !== 200) {
18596
+ return { status: false, error: response.message || `code ${response.code}` };
18597
+ }
18598
+ const raw = Array.isArray(response.data)
18599
+ ? response.data
18600
+ : Array.isArray(response.data?.data)
18601
+ ? response.data.data
18602
+ : [];
18603
+ const weiToNormal = (v) => {
18604
+ if (v === undefined || v === null || v === "")
18605
+ return "0";
18606
+ const bn = new BigNumber__default["default"](String(v));
18607
+ if (!bn.isFinite() || bn.isZero())
18608
+ return "0";
18609
+ return bn.dividedBy(new BigNumber__default["default"](10).pow(18)).toString();
18610
+ };
18611
+ const bars = raw.map((row) => {
18612
+ if (Array.isArray(row)) {
18613
+ const [time, open, high, low, close, volume] = row;
18614
+ return {
18615
+ time: Number(time ?? 0),
18616
+ open: weiToNormal(open),
18617
+ high: weiToNormal(high),
18618
+ low: weiToNormal(low),
18619
+ close: weiToNormal(close),
18620
+ volume: weiToNormal(volume),
18621
+ };
18622
+ }
18623
+ return {
18624
+ time: Number(row.time ?? row.timestamp ?? 0),
18625
+ open: weiToNormal(row.open),
18626
+ high: weiToNormal(row.high),
18627
+ low: weiToNormal(row.low),
18628
+ close: weiToNormal(row.close),
18629
+ volume: weiToNormal(row.volume ?? row.vol),
18630
+ };
18631
+ });
18632
+ return { status: true, data: bars };
18633
+ }
18634
+ catch (error) {
18635
+ return { status: false, error: formatError(error) };
18636
+ }
18637
+ }
18561
18638
  /**
18562
18639
  * Add isolated margin to an existing position (on-chain)
18563
18640
  * @param params Margin adjustment parameters
@@ -18620,9 +18697,14 @@ class DipCoinPerpSDK {
18620
18697
  }
18621
18698
  async buildMarginTransaction(params, action) {
18622
18699
  const payload = this.buildMarginCallArgs(params, action);
18623
- // v6+ uses add_margin_v3/remove_margin_v3 which read &PriceFeed (a shared object
18624
- // maintained off-band by an operator). No Pyth update is needed on the user-side tx.
18700
+ // v6+ requires PriceFeed to be fresh (~60s) before the margin call. Operator updates
18701
+ // are only hourly so we always fetch + prepend our own update_price_feed step.
18702
+ const sym = payload.market || params.symbol;
18703
+ if (!sym)
18704
+ throw new Error("symbol/market required to refresh PriceFeed before margin op");
18705
+ const feeds = await this.fetchSignedPriceFeeds([sym]);
18625
18706
  const baseTx = new transactions.Transaction();
18707
+ this.appendPriceFeedUpdates(baseTx, Array.from(feeds.values()));
18626
18708
  if (action === "add") {
18627
18709
  return buildAddMarginTx(this.deploymentConfig, payload, baseTx, params.gasBudget);
18628
18710
  }
@@ -18915,6 +18997,63 @@ class DipCoinPerpSDK {
18915
18997
  }
18916
18998
  return id;
18917
18999
  }
19000
+ /**
19001
+ * Fetch signed price update bytes for the given markets from the DipCoin pricing service.
19002
+ * Each entry's payload/signature/publicKey is consumed by
19003
+ * `<pkg>::price_feed::update_price_feed(PriceFeed, Clock, payload, signature, publicKey)`.
19004
+ *
19005
+ * The on-chain v6 contract requires the PriceFeed entry for a perp to be fresh (~60s)
19006
+ * for any margin/vault NAV write. The CLI prepends one update_price_feed call per
19007
+ * perp involved in the tx before the actual margin/vault call.
19008
+ */
19009
+ async fetchSignedPriceFeeds(symbols) {
19010
+ const out = new Map();
19011
+ if (symbols.length === 0)
19012
+ return out;
19013
+ const qs = symbols.map((s) => `symbols=${encodeURIComponent(s)}`).join("&");
19014
+ const resp = await this.httpClient.get(`${API_ENDPOINTS.SIGNED_PRICE_LATEST}?${qs}`);
19015
+ if (resp.code !== 200 || !resp.data) {
19016
+ throw new Error(`Failed to fetch signed price feed for [${symbols.join(",")}]: ${resp.message || `code ${resp.code}`}`);
19017
+ }
19018
+ const d = resp.data;
19019
+ const b64 = (s) => Uint8Array.from(Buffer.from(s, "base64"));
19020
+ if (!d.payload || !d.signature || !d.publicKey) {
19021
+ throw new Error("Signed price feed response missing payload/signature/publicKey");
19022
+ }
19023
+ const entry = { payload: b64(d.payload), signature: b64(d.signature), publicKey: b64(d.publicKey) };
19024
+ // API returns a single bundle covering all requested symbols' prices in priceList,
19025
+ // but only one (payload, signature, publicKey) tuple. Map every requested symbol to it.
19026
+ for (const sym of symbols)
19027
+ out.set(sym, entry);
19028
+ return out;
19029
+ }
19030
+ /**
19031
+ * Build a `price_feed::update_price_feed` PTB command for each provided signed feed
19032
+ * entry and append to the given transaction.
19033
+ */
19034
+ appendPriceFeedUpdates(tx, feeds) {
19035
+ const pkg = this.getPackageId();
19036
+ const pfId = this.getPriceFeedId();
19037
+ for (const f of feeds) {
19038
+ tx.moveCall({
19039
+ target: `${pkg}::price_feed::update_price_feed`,
19040
+ arguments: [
19041
+ tx.object(pfId),
19042
+ tx.object("0x6"),
19043
+ tx.pure.vector("u8", Array.from(f.payload)),
19044
+ tx.pure.vector("u8", Array.from(f.signature)),
19045
+ tx.pure.vector("u8", Array.from(f.publicKey)),
19046
+ ],
19047
+ typeArguments: [],
19048
+ });
19049
+ }
19050
+ }
19051
+ getPackageId() {
19052
+ const pkgs = this.deploymentConfig?.packages;
19053
+ if (!pkgs || !pkgs.length)
19054
+ throw new Error("Deployment config missing packages array");
19055
+ return pkgs[pkgs.length - 1];
19056
+ }
18918
19057
  async getOnChainPerpetualIds() {
18919
19058
  const pcId = this.getDeploymentProtocolConfigId();
18920
19059
  const df = await this.suiClient.getDynamicFieldObject({
@@ -18935,12 +19074,16 @@ class DipCoinPerpSDK {
18935
19074
  const t = tx || new transactions.Transaction();
18936
19075
  const vaultPkg = this.getVaultPackageId();
18937
19076
  const currencyType = this.getCurrencyType();
18938
- // v6+: PriceFeed is a single shared object maintained by an operator off-band.
18939
- // new_vault_nav populates `perpetuals` from ProtocolConfig.perpetual_registry, and
18940
- // is_nav_filled requires EVERY registered perpetual to have a position value entry.
18941
- // So we must compute against the on-chain registry, not the local deployment.markets
18942
- // list (which can drift behind chain additions).
19077
+ // v6+: PriceFeed must be fresh for every perp whose value compute_perpetual_position_value_v2
19078
+ // will touch. Refresh ALL markets first by fetching one combined signed bundle and prepending
19079
+ // a single update_price_feed step. Then iterate the on-chain perpetual_registry (which is
19080
+ // what new_vault_nav populates, and is_nav_filled requires every entry to be computed).
18943
19081
  const priceFeedId = this.getPriceFeedId();
19082
+ const allSymbols = this.getMarketSymbols();
19083
+ const feeds = await this.fetchSignedPriceFeeds(allSymbols);
19084
+ // Dedup the bundle entries (the API returns one tuple covering all requested symbols).
19085
+ const uniqFeeds = Array.from(new Map(Array.from(feeds.values()).map((f) => [Buffer.from(f.payload).toString("hex"), f])).values());
19086
+ this.appendPriceFeedUpdates(t, uniqFeeds);
18944
19087
  const perpetualIds = await this.getOnChainPerpetualIds();
18945
19088
  const [nav] = t.moveCall({
18946
19089
  target: `${vaultPkg}::vault::new_vault_nav`,
@@ -19821,6 +19964,34 @@ function registerTradeCommands(program) {
19821
19964
  .argument("<symbol>", "Trading pair (e.g. BTC or BTC-PERP)")
19822
19965
  .argument("<amount>", "USDC margin amount (e.g. 100 or 100USDC)")
19823
19966
  .argument("<leverage>", "Leverage multiplier (e.g. 10 or 10x)")).action((symbol, amount, leverage, opts) => placeOrder(program, OrderSide.SELL, symbol, amount, leverage, opts));
19967
+ trade
19968
+ .command("leverage")
19969
+ .description("Adjust default leverage for a market (e.g. trade leverage BTC 10)")
19970
+ .argument("<symbol>", "Trading pair (e.g. BTC or BTC-PERP)")
19971
+ .argument("<leverage>", "Leverage multiplier (e.g. 10 or 10x)")
19972
+ .action(async (symbol, leverage) => {
19973
+ try {
19974
+ symbol = normalizeSymbol(symbol);
19975
+ const lev = Number(String(leverage).replace(/x$/i, ""));
19976
+ if (!Number.isFinite(lev) || lev <= 0)
19977
+ return handleError(`Invalid leverage: ${leverage}`);
19978
+ const sdk = getSDK();
19979
+ // Only ISOLATED margin is supported by the backend today.
19980
+ const result = await sdk.adjustLeverage({
19981
+ symbol,
19982
+ leverage: lev,
19983
+ marginType: "ISOLATED",
19984
+ });
19985
+ if (!result.status)
19986
+ return handleError(result.error);
19987
+ if (isJson(program))
19988
+ return printJson(result.data);
19989
+ console.log(`Leverage for ${symbol} set to ${lev}x (ISOLATED).`);
19990
+ }
19991
+ catch (e) {
19992
+ handleError(e);
19993
+ }
19994
+ });
19824
19995
  trade
19825
19996
  .command("cancel")
19826
19997
  .description("Cancel orders by hash")
@@ -20128,6 +20299,48 @@ function registerMarketCommands(program) {
20128
20299
  handleError(e);
20129
20300
  }
20130
20301
  });
20302
+ market
20303
+ .command("kline")
20304
+ .description("Get kline (candlestick) history for a symbol")
20305
+ .argument("<symbol>", "Trading pair")
20306
+ .option("--interval <i>", "Bar interval: 1m / 5m / 15m / 30m / 1h / 4h / 1d / 1w (default: 1h)", "1h")
20307
+ .option("--from <ts>", "Start unix timestamp in seconds (default: 24h ago)")
20308
+ .option("--to <ts>", "End unix timestamp in seconds (default: now)")
20309
+ .option("--count <n>", "Cap on number of bars returned (most recent first)")
20310
+ .action(async (symbol, opts) => {
20311
+ try {
20312
+ symbol = normalizeSymbol(symbol);
20313
+ const now = Math.floor(Date.now() / 1000);
20314
+ const from = opts.from !== undefined ? Number(opts.from) : now - 86400;
20315
+ const to = opts.to !== undefined ? Number(opts.to) : now;
20316
+ const countback = opts.count !== undefined ? Number(opts.count) : undefined;
20317
+ const sdk = getSDK();
20318
+ const result = await sdk.getKlineHistory({
20319
+ symbol,
20320
+ interval: String(opts.interval),
20321
+ from,
20322
+ to,
20323
+ countback,
20324
+ });
20325
+ if (!result.status || !result.data)
20326
+ return handleError(result.error);
20327
+ if (isJson(program))
20328
+ return printJson(result.data);
20329
+ if (!result.data.length)
20330
+ return console.log("No bars.");
20331
+ printTable(["Time (UTC)", "Open", "High", "Low", "Close", "Volume"], result.data.map((b) => [
20332
+ new Date(b.time * 1000).toISOString().replace("T", " ").slice(0, 16),
20333
+ b.open,
20334
+ b.high,
20335
+ b.low,
20336
+ b.close,
20337
+ b.volume,
20338
+ ]));
20339
+ }
20340
+ catch (e) {
20341
+ handleError(e);
20342
+ }
20343
+ });
20131
20344
  }
20132
20345
 
20133
20346
  function registerHistoryCommands(program) {
@@ -20556,11 +20769,12 @@ function registerVaultCommands(program) {
20556
20769
  });
20557
20770
  // === vault fill ===
20558
20771
  vault
20559
- .command("fill")
20560
- .description("Fill pending withdrawal requests (e.g. vault fill <vaultId> <requestIDs...>)")
20772
+ .command("fill-withdrawals")
20773
+ .alias("fill")
20774
+ .description("Fill pending withdrawal requests (creator/operator only) — e.g. vault fill-withdrawals <vaultId> <requestIDs...>")
20561
20775
  .argument("<vaultId>", "Vault object ID")
20562
20776
  .argument("<requestIDs...>", "Withdrawal request object IDs")
20563
- .option("--markets <ids>", "Comma-separated market PerpetualIDs for share price update")
20777
+ .option("--markets <ids>", "Comma-separated market PerpetualIDs (deprecated; NAV now uses on-chain registry)")
20564
20778
  .action(async (vaultId, requestIDs, opts) => {
20565
20779
  try {
20566
20780
  const sdk = getSDK();