@neutral-trade/sdk 0.2.13 → 0.3.3

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
@@ -147,7 +147,16 @@ This is simpler and doesn't require an additional network request at initializat
147
147
 
148
148
  ## Examples
149
149
 
150
- Check out the [examples](./examples) directory for more usage examples.
150
+ See the [examples](./examples) directory. From the `sdk/` package root (clone this repo):
151
+
152
+ ```bash
153
+ # Local validator default RPC: http://127.0.0.1:8899 — override with SOLANA_RPC_URL
154
+ export SOLANA_KEYPAIR_PATH="$HOME/.config/solana/id.json"
155
+ pnpm example:devnet:deposit
156
+ pnpm example:devnet:withdraw
157
+ ```
158
+
159
+ Uses the built-in devnet registry (`src/registry/vaults.devnet.json`, e.g. vault `100000001`). Your RPC must serve that vault on-chain; override vault id with `DEVNET_BUNDLE_VAULT_ID` if you use `NeutralTrade.create({ registry: [...] })` patterns in a forked script.
151
160
 
152
161
  ## License
153
162
 
package/dist/index.d.mts CHANGED
@@ -3377,18 +3377,18 @@ declare class NeutralTrade {
3377
3377
  userAddress: string;
3378
3378
  }): Promise<UserBalanceResult>;
3379
3379
  /**
3380
- * Build deposit instructions (optional init + requestDeposit). Fetches vault state on-chain.
3380
+ * Build deposit instructions (`initializeBundleDepositor` when needed, then `requestDeposit`).
3381
+ * Fetches bundle and user bundle accounts on-chain.
3381
3382
  */
3382
3383
  buildDepositInstructions({
3383
3384
  vaultId,
3384
3385
  userAddress,
3385
- amount,
3386
- needsInit
3386
+ amountRaw
3387
3387
  }: {
3388
3388
  vaultId: number;
3389
3389
  userAddress: string;
3390
- amount: number;
3391
- needsInit?: boolean;
3390
+ /** Smallest token units (decimal string), same scale as SPL `amount`. */
3391
+ amountRaw: string;
3392
3392
  }): Promise<TransactionInstruction[]>;
3393
3393
  /**
3394
3394
  * Build request-withdraw instruction. Fetches vault, oracle, and depositor accounts on-chain.
@@ -3396,41 +3396,69 @@ declare class NeutralTrade {
3396
3396
  buildRequestWithdrawInstruction({
3397
3397
  vaultId,
3398
3398
  userAddress,
3399
- amount
3399
+ amountRaw
3400
3400
  }: {
3401
3401
  vaultId: number;
3402
3402
  userAddress: string;
3403
- amount: number;
3403
+ /** Smallest token units (decimal string) to request withdrawing. */
3404
+ amountRaw: string;
3404
3405
  }): Promise<TransactionInstruction>;
3405
3406
  }
3406
3407
  //#endregion
3408
+ //#region src/utils/amount-raw.d.ts
3409
+ /**
3410
+ * Parse `amountRaw`: base-10 digits only, smallest token units (same scale as SPL `amount`).
3411
+ * @throws Error with message `INVALID_AMOUNT_RAW` on invalid input
3412
+ */
3413
+ declare function parseAmountRawToBigInt(amountRaw: string): bigint;
3414
+ /**
3415
+ * Legacy UI path: finite `human` × 10^`decimals`, rounded to nearest integer token unit.
3416
+ * Prefer integrators sending `amountRaw` from a decimal string instead of this helper.
3417
+ * @throws Error with message `INVALID_HUMAN_AMOUNT` when out of range or non-finite
3418
+ */
3419
+ declare function humanFloatToAmountRawString(human: number, decimals: number): string;
3420
+ //#endregion
3407
3421
  //#region src/utils/bundle-instructions.d.ts
3408
3422
  interface BuildBundleDepositInstructionsParams {
3409
3423
  bundleProgram: Program<Ntbundle>;
3410
3424
  bundleCluster?: BundleCluster;
3411
3425
  vault: VaultRegistryEntry;
3412
3426
  user: PublicKey;
3413
- /** UI token amount (multiplied by 10**on-chain decimals inside). */
3414
- amount: number;
3415
- needsInit?: boolean;
3427
+ /** Smallest token units (decimal string), same scale as SPL token `amount`. */
3428
+ amountRaw: string;
3416
3429
  }
3417
3430
  interface BuildBundleRequestWithdrawInstructionParams {
3418
3431
  bundleProgram: Program<Ntbundle>;
3419
3432
  bundleCluster?: BundleCluster;
3420
3433
  vault: VaultRegistryEntry;
3421
3434
  user: PublicKey;
3422
- amount: number;
3435
+ /** Smallest token units (decimal string) to request withdrawing. */
3436
+ amountRaw: string;
3423
3437
  }
3424
3438
  /**
3425
- * Optional `initializeBundleDepositor` + `requestDeposit`. Fetches bundle account internally.
3439
+ * Shares to burn for `requestWithdrawal`, from integer token raw and on-chain totals.
3440
+ * Full position: pass `amountRaw >= userTokenRaw` where `userTokenRaw = (userShares * totalEquity) / totalShares`.
3441
+ */
3442
+ declare function computeRequestWithdrawalSharesFromAmountRaw({
3443
+ amountRaw,
3444
+ userShares,
3445
+ totalEquity,
3446
+ totalShares
3447
+ }: {
3448
+ amountRaw: bigint;
3449
+ userShares: BN;
3450
+ totalEquity: bigint;
3451
+ totalShares: bigint;
3452
+ }): BN;
3453
+ /**
3454
+ * `initializeBundleDepositor` (when missing) + `requestDeposit`. Fetches bundle + user bundle internally.
3426
3455
  */
3427
3456
  declare function buildBundleDepositInstructions({
3428
3457
  bundleProgram,
3429
3458
  bundleCluster,
3430
3459
  vault,
3431
3460
  user,
3432
- amount,
3433
- needsInit
3461
+ amountRaw
3434
3462
  }: BuildBundleDepositInstructionsParams): Promise<TransactionInstruction[]>;
3435
3463
  /**
3436
3464
  * `requestWithdrawal` only. Fetches bundle, oracle, and user bundle internally.
@@ -3440,7 +3468,7 @@ declare function buildBundleRequestWithdrawInstruction({
3440
3468
  bundleCluster,
3441
3469
  vault,
3442
3470
  user,
3443
- amount
3471
+ amountRaw
3444
3472
  }: BuildBundleRequestWithdrawInstructionParams): Promise<TransactionInstruction>;
3445
3473
  //#endregion
3446
3474
  //#region src/utils/pda.d.ts
@@ -3470,4 +3498,4 @@ declare function derivePendingAuthPDA(bundlePDA: PublicKey, programId: PublicKey
3470
3498
  */
3471
3499
  declare function getVaultDepositorAddressSync(programId: PublicKey, vault: PublicKey, authority: PublicKey): PublicKey;
3472
3500
  //#endregion
3473
- export { BUNDLE_PROGRAM_ID_V2_MAINNET, type BundleAccount, type BundleCluster, type BundleProgram, type BundleProvider, DEFAULT_BUNDLE_PROGRAM_ID_DEVNET, DEFAULT_BUNDLE_PROGRAM_ID_MAINNET, DevnetVaultId, NeutralTrade, type NeutralTradeConfig, type NeutralTradeCoreContext, type OracleData, SEED_ORACLE, SEED_PENDING_AUTH, SEED_TEMP, SEED_USER, SupportedChain, SupportedToken, type Token, type UserBalanceResult, type UserBundleAccount, type UserBundleTempData, type VaultBalanceData, VaultCategory, type VaultRegistryEntry as VaultConfig, type VaultRegistry as VaultConfigRecord, VaultId, VaultType, buildBundleDepositInstructions, buildBundleRequestWithdrawInstruction, createBundleProgramById, createDummyWallet, deriveOraclePDA, derivePendingAuthPDA, deriveTempDataPDA, deriveUserPDA, getBundleProgramId, getDefaultBundleProgramIdByCluster, getSolanaTokenDecimals, getSolanaTokenMint, getVaultByAddress, getVaultById, getVaultDepositorAddressSync, getVaultRegistry, isValidVaultAddress, tokens, vaults, vaultsDevnet };
3501
+ export { BUNDLE_PROGRAM_ID_V2_MAINNET, type BuildBundleDepositInstructionsParams, type BuildBundleRequestWithdrawInstructionParams, type BundleAccount, type BundleCluster, type BundleProgram, type BundleProvider, DEFAULT_BUNDLE_PROGRAM_ID_DEVNET, DEFAULT_BUNDLE_PROGRAM_ID_MAINNET, DevnetVaultId, NeutralTrade, type NeutralTradeConfig, type NeutralTradeCoreContext, type OracleData, SEED_ORACLE, SEED_PENDING_AUTH, SEED_TEMP, SEED_USER, SupportedChain, SupportedToken, type Token, type UserBalanceResult, type UserBundleAccount, type UserBundleTempData, type VaultBalanceData, VaultCategory, type VaultRegistryEntry as VaultConfig, type VaultRegistry as VaultConfigRecord, VaultId, VaultType, buildBundleDepositInstructions, buildBundleRequestWithdrawInstruction, computeRequestWithdrawalSharesFromAmountRaw, createBundleProgramById, createDummyWallet, deriveOraclePDA, derivePendingAuthPDA, deriveTempDataPDA, deriveUserPDA, getBundleProgramId, getDefaultBundleProgramIdByCluster, getSolanaTokenDecimals, getSolanaTokenMint, getVaultByAddress, getVaultById, getVaultDepositorAddressSync, getVaultRegistry, humanFloatToAmountRawString, isValidVaultAddress, parseAmountRawToBigInt, tokens, vaults, vaultsDevnet };
package/dist/index.mjs CHANGED
@@ -13085,14 +13085,50 @@ async function getBundleBalances({ vaultIds, userAddress, vaults: vaults$1, bund
13085
13085
  return result;
13086
13086
  }
13087
13087
 
13088
+ //#endregion
13089
+ //#region src/utils/amount-raw.ts
13090
+ /** Max SPL token amount we accept (fits u64). */
13091
+ const U64_MAX = 18446744073709551615n;
13092
+ /**
13093
+ * Parse `amountRaw`: base-10 digits only, smallest token units (same scale as SPL `amount`).
13094
+ * @throws Error with message `INVALID_AMOUNT_RAW` on invalid input
13095
+ */
13096
+ function parseAmountRawToBigInt(amountRaw) {
13097
+ const t = amountRaw.trim();
13098
+ if (!/^\d+$/.test(t)) throw new Error("INVALID_AMOUNT_RAW");
13099
+ const v = BigInt(t);
13100
+ if (v <= 0n) throw new Error("INVALID_AMOUNT_RAW");
13101
+ if (v > U64_MAX) throw new Error("INVALID_AMOUNT_RAW");
13102
+ return v;
13103
+ }
13104
+ /**
13105
+ * Legacy UI path: finite `human` × 10^`decimals`, rounded to nearest integer token unit.
13106
+ * Prefer integrators sending `amountRaw` from a decimal string instead of this helper.
13107
+ * @throws Error with message `INVALID_HUMAN_AMOUNT` when out of range or non-finite
13108
+ */
13109
+ function humanFloatToAmountRawString(human, decimals) {
13110
+ if (!Number.isFinite(human) || human <= 0) throw new Error("INVALID_HUMAN_AMOUNT");
13111
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 18) throw new Error("INVALID_HUMAN_AMOUNT");
13112
+ const scaled = human * 10 ** decimals;
13113
+ if (!Number.isFinite(scaled)) throw new Error("INVALID_HUMAN_AMOUNT");
13114
+ const rounded = Math.round(scaled);
13115
+ if (rounded <= 0) throw new Error("INVALID_HUMAN_AMOUNT");
13116
+ return parseAmountRawToBigInt(BigInt(rounded).toString()).toString();
13117
+ }
13118
+
13088
13119
  //#endregion
13089
13120
  //#region src/utils/bundle-instructions.ts
13090
- /** Same share math as legacy app `useBundleRequestWithdrawMutation`. */
13091
- function computeRequestWithdrawalSharesAmount({ amount, userVaultBalance, pps, assetDecimals, userShares }) {
13092
- let sharesAmount;
13093
- if (Number(amount) >= Number(userVaultBalance)) sharesAmount = userShares;
13094
- else sharesAmount = new BN(Math.floor(amount * 10 ** assetDecimals / pps));
13095
- return sharesAmount.lte(userShares) ? sharesAmount : userShares;
13121
+ /**
13122
+ * Shares to burn for `requestWithdrawal`, from integer token raw and on-chain totals.
13123
+ * Full position: pass `amountRaw >= userTokenRaw` where `userTokenRaw = (userShares * totalEquity) / totalShares`.
13124
+ */
13125
+ function computeRequestWithdrawalSharesFromAmountRaw({ amountRaw, userShares, totalEquity, totalShares }) {
13126
+ const us = BigInt(userShares.toString());
13127
+ if (totalShares === 0n || us === 0n) return new BN(0);
13128
+ if (amountRaw >= us * totalEquity / totalShares) return userShares;
13129
+ if (totalEquity === 0n) return new BN(0);
13130
+ const sharesBn = new BN((amountRaw * totalShares / totalEquity).toString());
13131
+ return sharesBn.gt(userShares) ? userShares : sharesBn;
13096
13132
  }
13097
13133
  function assertBundleVault(vault) {
13098
13134
  if (vault.type !== VaultType.Bundle) throw new Error(`Vault ${vault.vaultId} is not a Bundle vault`);
@@ -13103,18 +13139,21 @@ function bundleProgramIdForVault(vault, bundleCluster = "mainnet") {
13103
13139
  return id;
13104
13140
  }
13105
13141
  /**
13106
- * Optional `initializeBundleDepositor` + `requestDeposit`. Fetches bundle account internally.
13142
+ * `initializeBundleDepositor` (when missing) + `requestDeposit`. Fetches bundle + user bundle internally.
13107
13143
  */
13108
- async function buildBundleDepositInstructions({ bundleProgram, bundleCluster = "mainnet", vault, user, amount, needsInit = false }) {
13144
+ async function buildBundleDepositInstructions({ bundleProgram, bundleCluster = "mainnet", vault, user, amountRaw }) {
13109
13145
  assertBundleVault(vault);
13110
13146
  const bundlePDA = new PublicKey(vault.vaultAddress);
13111
13147
  const programId = bundleProgramIdForVault(vault, bundleCluster);
13112
13148
  if (programId !== bundleProgram.programId.toBase58()) throw new Error(`Vault ${vault.vaultId} program id mismatch: vault=${programId}, client=${bundleProgram.programId.toBase58()}`);
13113
- const bundleInfo = await bundleProgram.account.bundle.fetch(bundlePDA);
13114
- const depositAmountBN = new BN(Math.floor(amount * 10 ** bundleInfo.assetDecimals));
13115
13149
  const programPk = bundleProgram.programId;
13116
- const oraclePDA = deriveOraclePDA(bundlePDA, programPk);
13117
13150
  const userPDA = deriveUserPDA(user, bundlePDA, programPk);
13151
+ const [bundleAcc, userBundleAcc] = await bundleProgram.provider.connection.getMultipleAccountsInfo([bundlePDA, userPDA]);
13152
+ if (!bundleAcc?.data?.length) throw new Error(`Bundle account not found for vault ${vault.vaultId}`);
13153
+ const bundleInfo = bundleProgram.coder.accounts.decode("bundle", bundleAcc.data);
13154
+ const needsInit = (userBundleAcc?.data?.length ? bundleProgram.coder.accounts.decode("userBundleAccount", userBundleAcc.data) : null) === null;
13155
+ const depositAmountBn = new BN(parseAmountRawToBigInt(amountRaw).toString());
13156
+ const oraclePDA = deriveOraclePDA(bundlePDA, programPk);
13118
13157
  const tempDataPDA = deriveTempDataPDA(bundlePDA, programPk);
13119
13158
  const pendingAuthPDA = derivePendingAuthPDA(bundlePDA, programPk);
13120
13159
  const userTokenAcct = getAssociatedTokenAddressSync(bundleInfo.assetAddress, user, true);
@@ -13130,7 +13169,7 @@ async function buildBundleDepositInstructions({ bundleProgram, bundleCluster = "
13130
13169
  }).instruction();
13131
13170
  instructions$2.push(initIx);
13132
13171
  }
13133
- const depositIx = await bundleProgram.methods.requestDeposit(depositAmountBN).accounts({
13172
+ const depositIx = await bundleProgram.methods.requestDeposit(depositAmountBn).accounts({
13134
13173
  user,
13135
13174
  userTokenAccount: userTokenAcct,
13136
13175
  pendingDepositTokenAccount: pendingTokenAcct,
@@ -13150,7 +13189,7 @@ async function buildBundleDepositInstructions({ bundleProgram, bundleCluster = "
13150
13189
  /**
13151
13190
  * `requestWithdrawal` only. Fetches bundle, oracle, and user bundle internally.
13152
13191
  */
13153
- async function buildBundleRequestWithdrawInstruction({ bundleProgram, bundleCluster = "mainnet", vault, user, amount }) {
13192
+ async function buildBundleRequestWithdrawInstruction({ bundleProgram, bundleCluster = "mainnet", vault, user, amountRaw }) {
13154
13193
  assertBundleVault(vault);
13155
13194
  const bundlePDA = new PublicKey(vault.vaultAddress);
13156
13195
  const programId = bundleProgramIdForVault(vault, bundleCluster);
@@ -13164,19 +13203,13 @@ async function buildBundleRequestWithdrawInstruction({ bundleProgram, bundleClus
13164
13203
  bundleProgram.account.oracleData.fetch(oraclePDA),
13165
13204
  bundleProgram.account.userBundleAccount.fetch(userPDA)
13166
13205
  ]);
13167
- const pps = calculateOnChainPps({
13168
- oracleAverageExternalEquity: BigInt(oracleData.averageExternalEquity.toString()),
13169
- bundleUnderlyingBalance: BigInt(bundleAccount.bundleUnderlyingBalance.toString()),
13170
- totalShares: BigInt(bundleAccount.totalShares.toString())
13171
- });
13172
- const divisor = 10 ** bundleAccount.assetDecimals;
13173
- const userSharesNum = Number(userBundle.shares.toString());
13174
- const sharesAmount = computeRequestWithdrawalSharesAmount({
13175
- amount,
13176
- userVaultBalance: Math.round(userSharesNum * pps) / divisor,
13177
- pps,
13178
- assetDecimals: bundleAccount.assetDecimals,
13179
- userShares: new BN(userBundle.shares.toString())
13206
+ const totalEquity = BigInt(oracleData.averageExternalEquity.toString()) + BigInt(bundleAccount.bundleUnderlyingBalance.toString());
13207
+ const totalShares = BigInt(bundleAccount.totalShares.toString());
13208
+ const sharesAmount = computeRequestWithdrawalSharesFromAmountRaw({
13209
+ amountRaw: parseAmountRawToBigInt(amountRaw),
13210
+ userShares: new BN(userBundle.shares.toString()),
13211
+ totalEquity,
13212
+ totalShares
13180
13213
  });
13181
13214
  return await bundleProgram.methods.requestWithdrawal(sharesAmount).accounts({
13182
13215
  user,
@@ -13358,9 +13391,10 @@ var NeutralTrade = class NeutralTrade {
13358
13391
  });
13359
13392
  }
13360
13393
  /**
13361
- * Build deposit instructions (optional init + requestDeposit). Fetches vault state on-chain.
13394
+ * Build deposit instructions (`initializeBundleDepositor` when needed, then `requestDeposit`).
13395
+ * Fetches bundle and user bundle accounts on-chain.
13362
13396
  */
13363
- async buildDepositInstructions({ vaultId, userAddress, amount, needsInit = false }) {
13397
+ async buildDepositInstructions({ vaultId, userAddress, amountRaw }) {
13364
13398
  const vault = this.vaults[vaultId];
13365
13399
  if (!vault) throw new Error(`Vault config not found for vaultId ${vaultId}`);
13366
13400
  return buildBundleDepositInstructions({
@@ -13368,14 +13402,13 @@ var NeutralTrade = class NeutralTrade {
13368
13402
  bundleCluster: this.bundleCluster,
13369
13403
  vault,
13370
13404
  user: new PublicKey(userAddress),
13371
- amount,
13372
- needsInit
13405
+ amountRaw
13373
13406
  });
13374
13407
  }
13375
13408
  /**
13376
13409
  * Build request-withdraw instruction. Fetches vault, oracle, and depositor accounts on-chain.
13377
13410
  */
13378
- async buildRequestWithdrawInstruction({ vaultId, userAddress, amount }) {
13411
+ async buildRequestWithdrawInstruction({ vaultId, userAddress, amountRaw }) {
13379
13412
  const vault = this.vaults[vaultId];
13380
13413
  if (!vault) throw new Error(`Vault config not found for vaultId ${vaultId}`);
13381
13414
  return buildBundleRequestWithdrawInstruction({
@@ -13383,10 +13416,10 @@ var NeutralTrade = class NeutralTrade {
13383
13416
  bundleCluster: this.bundleCluster,
13384
13417
  vault,
13385
13418
  user: new PublicKey(userAddress),
13386
- amount
13419
+ amountRaw
13387
13420
  });
13388
13421
  }
13389
13422
  };
13390
13423
 
13391
13424
  //#endregion
13392
- export { BUNDLE_PROGRAM_ID_V2_MAINNET, DEFAULT_BUNDLE_PROGRAM_ID_DEVNET, DEFAULT_BUNDLE_PROGRAM_ID_MAINNET, DevnetVaultId, NeutralTrade, SEED_ORACLE, SEED_PENDING_AUTH, SEED_TEMP, SEED_USER, SupportedChain, SupportedToken, VaultCategory, VaultId, VaultType, buildBundleDepositInstructions, buildBundleRequestWithdrawInstruction, createBundleProgramById, createDummyWallet, deriveOraclePDA, derivePendingAuthPDA, deriveTempDataPDA, deriveUserPDA, getBundleProgramId, getDefaultBundleProgramIdByCluster, getSolanaTokenDecimals, getSolanaTokenMint, getVaultByAddress, getVaultById, getVaultDepositorAddressSync, getVaultRegistry, isValidVaultAddress, tokens, vaults, vaultsDevnet };
13425
+ export { BUNDLE_PROGRAM_ID_V2_MAINNET, DEFAULT_BUNDLE_PROGRAM_ID_DEVNET, DEFAULT_BUNDLE_PROGRAM_ID_MAINNET, DevnetVaultId, NeutralTrade, SEED_ORACLE, SEED_PENDING_AUTH, SEED_TEMP, SEED_USER, SupportedChain, SupportedToken, VaultCategory, VaultId, VaultType, buildBundleDepositInstructions, buildBundleRequestWithdrawInstruction, computeRequestWithdrawalSharesFromAmountRaw, createBundleProgramById, createDummyWallet, deriveOraclePDA, derivePendingAuthPDA, deriveTempDataPDA, deriveUserPDA, getBundleProgramId, getDefaultBundleProgramIdByCluster, getSolanaTokenDecimals, getSolanaTokenMint, getVaultByAddress, getVaultById, getVaultDepositorAddressSync, getVaultRegistry, humanFloatToAmountRawString, isValidVaultAddress, parseAmountRawToBigInt, tokens, vaults, vaultsDevnet };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@neutral-trade/sdk",
3
3
  "type": "module",
4
- "version": "0.2.13",
4
+ "version": "0.3.3",
5
5
  "description": "SDK for Neutral Trade vaults",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/neutral-trade/sdk#readme",
@@ -69,6 +69,10 @@
69
69
  "lint": "eslint",
70
70
  "release": "bumpp",
71
71
  "start": "tsx src/index.ts",
72
+ "example:devnet:deposit": "tsx examples/devnet-deposit.ts",
73
+ "example:devnet:withdraw": "tsx examples/devnet-withdraw.ts",
74
+ "diagnose:balances": "tsx diagnose/vault-balance-fetch.ts",
75
+ "diagnose:deposit-conservation": "tsx diagnose/deposit-conservation.ts",
72
76
  "test": "vitest",
73
77
  "typecheck": "tsc",
74
78
  "docs:build": "typedoc",