@usherlabs/cex-broker 0.2.9 → 0.2.10

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.
@@ -313495,16 +313495,18 @@ if (process.env.LOG_LEVEL !== "debug") {
313495
313495
  var log = baseLogger;
313496
313496
 
313497
313497
  // src/helpers/index.ts
313498
- class WithdrawRoutingError extends Error {
313499
- }
313500
-
313501
- class WithdrawRoutingUnavailableError extends WithdrawRoutingError {
313502
- }
313503
- function isMasterBrokerAccount(account) {
313504
- return account.label === "primary" || account.role === "master";
313498
+ class BrokerAccountPreconditionError extends Error {
313499
+ constructor(message) {
313500
+ super(message);
313501
+ this.name = "BrokerAccountPreconditionError";
313502
+ }
313505
313503
  }
313506
- function isSubaccountBrokerAccount(account) {
313507
- return !isMasterBrokerAccount(account);
313504
+ function requireDestinationEmail(dest, transferType) {
313505
+ const email = dest.email?.trim();
313506
+ if (!email) {
313507
+ throw new BrokerAccountPreconditionError(`Destination account '${dest.label}' requires an email configured for ${transferType} transfers`);
313508
+ }
313509
+ return email;
313508
313510
  }
313509
313511
  function authenticateRequest(call, whitelistIps) {
313510
313512
  const clientIp = call.getPeer().split(":")[0];
@@ -313826,81 +313828,50 @@ function validateWithdraw(policy, exchange, network, recipientAddress, _amount,
313826
313828
  }
313827
313829
  return { valid: true };
313828
313830
  }
313829
- function normalizeAccountSelector(selector2, metadata, defaultSelector) {
313830
- const raw = selector2?.trim().toLowerCase() ?? defaultSelector;
313831
- if (raw === "current") {
313832
- return getCurrentBrokerSelector(metadata);
313833
- }
313834
- if (raw === "primary") {
313835
- return "primary";
313836
- }
313837
- const secondaryMatch = raw.match(/^secondary:(\d+)$/);
313838
- if (secondaryMatch) {
313839
- return `secondary:${secondaryMatch[1]}`;
313840
- }
313841
- throw new WithdrawRoutingError(`Invalid account selector "${selector2}"`);
313842
- }
313843
- async function transferBinanceSubAccountToMaster(source, code, amount) {
313831
+ async function transferBinanceInternal(source, dest, code, amount) {
313844
313832
  const exchange = source.exchange;
313845
- if (typeof exchange.sapiPostSubAccountTransferSubToMaster !== "function") {
313846
- throw new WithdrawRoutingUnavailableError("Binance sub-account to master transfer is unavailable in this CCXT build");
313847
- }
313848
313833
  await source.exchange.loadMarkets();
313849
313834
  const currency = source.exchange.currency(code);
313850
- return await exchange.sapiPostSubAccountTransferSubToMaster({
313851
- asset: currency.id,
313852
- amount: source.exchange.currencyToPrecision(code, amount)
313853
- });
313854
- }
313855
- async function executeWithdrawWithRouting(args) {
313856
- const {
313857
- cex: cex3,
313858
- brokers,
313859
- metadata,
313860
- selectedBroker,
313861
- code,
313862
- amount,
313863
- recipientAddress,
313864
- network,
313865
- params,
313866
- routeViaMaster,
313867
- sourceAccount,
313868
- masterAccount
313869
- } = args;
313870
- const withdrawParams = {
313871
- ...params ?? {},
313872
- network
313873
- };
313874
- if (!routeViaMaster) {
313875
- return await selectedBroker.withdraw(code, amount, recipientAddress, undefined, withdrawParams);
313876
- }
313877
- const sourceSelector = normalizeAccountSelector(sourceAccount, metadata, "current");
313878
- const masterSelector = normalizeAccountSelector(masterAccount, metadata, "primary");
313879
- if (!brokers) {
313880
- throw new WithdrawRoutingUnavailableError("Routed withdraw requires configured broker accounts");
313881
- }
313882
- const source = resolveBrokerAccount(brokers, sourceSelector);
313883
- if (!source) {
313884
- throw new WithdrawRoutingError(`Source account ${sourceSelector} is not configured`);
313885
- }
313886
- const master = resolveBrokerAccount(brokers, masterSelector);
313887
- if (!master) {
313888
- throw new WithdrawRoutingError(`Master account ${masterSelector} is not configured`);
313889
- }
313890
- if (!isMasterBrokerAccount(master)) {
313891
- throw new WithdrawRoutingError(`Master account ${masterSelector} must resolve to the primary/master account`);
313892
- }
313893
- if (source.label === master.label) {
313894
- return await master.exchange.withdraw(code, amount, recipientAddress, undefined, withdrawParams);
313835
+ const asset = currency.id;
313836
+ const amountStr = source.exchange.currencyToPrecision(code, amount);
313837
+ const isSourceSecondary = source.label.startsWith("secondary:");
313838
+ const isDestPrimary = dest.label === "primary";
313839
+ const isDestSecondary = dest.label.startsWith("secondary:");
313840
+ const isSourcePrimary = source.label === "primary";
313841
+ if (isSourceSecondary && isDestPrimary) {
313842
+ if (typeof exchange.sapiPostSubAccountTransferSubToMaster !== "function") {
313843
+ throw new Error("Binance sub→master transfer is unavailable in this CCXT build");
313844
+ }
313845
+ return await exchange.sapiPostSubAccountTransferSubToMaster({
313846
+ asset,
313847
+ amount: amountStr
313848
+ });
313895
313849
  }
313896
- if (!isSubaccountBrokerAccount(source)) {
313897
- throw new WithdrawRoutingError(`Source account ${sourceSelector} must resolve to a subaccount when routeViaMaster is enabled`);
313850
+ if (isSourceSecondary && isDestSecondary) {
313851
+ if (typeof exchange.sapiPostSubAccountTransferSubToSub !== "function") {
313852
+ throw new Error("Binance sub→sub transfer is unavailable in this CCXT build");
313853
+ }
313854
+ const destEmail = requireDestinationEmail(dest, "sub-to-sub");
313855
+ return await exchange.sapiPostSubAccountTransferSubToSub({
313856
+ toEmail: destEmail,
313857
+ asset,
313858
+ amount: amountStr
313859
+ });
313898
313860
  }
313899
- if (cex3.trim().toLowerCase() !== "binance") {
313900
- throw new WithdrawRoutingUnavailableError(`Withdraw routing via master is not supported for ${cex3}`);
313861
+ if (isSourcePrimary && isDestSecondary) {
313862
+ if (typeof exchange.sapiPostSubAccountUniversalTransfer !== "function") {
313863
+ throw new Error("Binance universal transfer is unavailable in this CCXT build");
313864
+ }
313865
+ const destEmail = requireDestinationEmail(dest, "primary-to-sub");
313866
+ return await exchange.sapiPostSubAccountUniversalTransfer({
313867
+ fromAccountType: "SPOT",
313868
+ toAccountType: "SPOT",
313869
+ toEmail: destEmail,
313870
+ asset,
313871
+ amount: amountStr
313872
+ });
313901
313873
  }
313902
- await transferBinanceSubAccountToMaster(source, code, amount);
313903
- return await master.exchange.withdraw(code, amount, recipientAddress, undefined, withdrawParams);
313874
+ throw new Error(`Unsupported transfer direction: ${source.label} ${dest.label}`);
313904
313875
  }
313905
313876
  function isMarketPatternMatch(pattern, broker, fromToken, toToken) {
313906
313877
  const normalizedPattern = pattern.toUpperCase().trim();
@@ -314383,7 +314354,8 @@ var Action = {
314383
314354
  FetchCurrency: 9,
314384
314355
  Call: 10,
314385
314356
  FetchAccountId: 11,
314386
- FetchFees: 12
314357
+ FetchFees: 12,
314358
+ InternalTransfer: 13
314387
314359
  };
314388
314360
 
314389
314361
  // src/proto/cex_broker/SubscriptionType.ts
@@ -314514,7 +314486,8 @@ var descriptor = {
314514
314486
  FetchCurrency: 9,
314515
314487
  Call: 10,
314516
314488
  FetchAccountId: 11,
314517
- FetchFees: 12
314489
+ FetchFees: 12,
314490
+ InternalTransfer: 13
314518
314491
  }
314519
314492
  }
314520
314493
  }
@@ -328100,11 +328073,13 @@ var WithdrawPayloadSchema = exports_external.object({
328100
328073
  recipientAddress: exports_external.string().min(1),
328101
328074
  amount: exports_external.coerce.number().positive(),
328102
328075
  chain: exports_external.string().min(1),
328103
- routeViaMaster: booleanLikeSchema.optional().default(false),
328104
- sourceAccount: exports_external.string().min(1).optional(),
328105
- masterAccount: exports_external.string().min(1).optional(),
328106
328076
  params: exports_external.preprocess(parseJsonString, stringNumberRecordSchema).default({})
328107
328077
  });
328078
+ var InternalTransferPayloadSchema = exports_external.object({
328079
+ amount: exports_external.coerce.number().positive(),
328080
+ fromAccount: exports_external.string().min(1).optional(),
328081
+ toAccount: exports_external.string().min(1).optional()
328082
+ });
328108
328083
  var CreateOrderPayloadSchema = exports_external.object({
328109
328084
  orderType: exports_external.enum(["market", "limit"]).default("limit"),
328110
328085
  amount: exports_external.coerce.number().positive(),
@@ -328152,6 +328127,31 @@ function safeLogError(context2, error48) {
328152
328127
  console.error(context2, error48);
328153
328128
  }
328154
328129
  }
328130
+ function mapCcxtErrorToGrpcStatus(error48) {
328131
+ if (error48 instanceof ccxt_default.AuthenticationError)
328132
+ return grpc.status.UNAUTHENTICATED;
328133
+ if (error48 instanceof ccxt_default.PermissionDenied)
328134
+ return grpc.status.PERMISSION_DENIED;
328135
+ if (error48 instanceof ccxt_default.InsufficientFunds)
328136
+ return grpc.status.FAILED_PRECONDITION;
328137
+ if (error48 instanceof ccxt_default.InvalidAddress)
328138
+ return grpc.status.INVALID_ARGUMENT;
328139
+ if (error48 instanceof ccxt_default.BadSymbol)
328140
+ return grpc.status.NOT_FOUND;
328141
+ if (error48 instanceof ccxt_default.BadRequest)
328142
+ return grpc.status.INVALID_ARGUMENT;
328143
+ if (error48 instanceof ccxt_default.NotSupported)
328144
+ return grpc.status.UNIMPLEMENTED;
328145
+ if (error48 instanceof ccxt_default.RateLimitExceeded)
328146
+ return grpc.status.RESOURCE_EXHAUSTED;
328147
+ if (error48 instanceof ccxt_default.OnMaintenance)
328148
+ return grpc.status.UNAVAILABLE;
328149
+ if (error48 instanceof ccxt_default.ExchangeNotAvailable)
328150
+ return grpc.status.UNAVAILABLE;
328151
+ if (error48 instanceof ccxt_default.NetworkError)
328152
+ return grpc.status.UNAVAILABLE;
328153
+ return;
328154
+ }
328155
328155
  function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, otelMetrics) {
328156
328156
  const server = new grpc.Server;
328157
328157
  server.addService(cexNode.cex_service.service, {
@@ -328207,7 +328207,8 @@ function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, ot
328207
328207
  message: "`action` AND `cex` fields are required"
328208
328208
  }, null);
328209
328209
  }
328210
- const broker = selectBroker(brokers[cex3], metadata) ?? createBroker(cex3, metadata);
328210
+ const normalizedCex = cex3.trim().toLowerCase();
328211
+ const broker = selectBroker(brokers[normalizedCex], metadata) ?? createBroker(normalizedCex, metadata);
328211
328212
  if (!broker) {
328212
328213
  return wrappedCallback({
328213
328214
  code: grpc.status.UNAUTHENTICATED,
@@ -328545,39 +328546,10 @@ function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, ot
328545
328546
  }, null);
328546
328547
  }
328547
328548
  try {
328548
- let transaction;
328549
- try {
328550
- transaction = await executeWithdrawWithRouting({
328551
- cex: cex3,
328552
- brokers: brokers[cex3],
328553
- metadata,
328554
- selectedBroker: broker,
328555
- code: symbol2,
328556
- amount: transferValue.amount,
328557
- recipientAddress: transferValue.recipientAddress,
328558
- network: transferValue.chain,
328559
- params: transferValue.params,
328560
- routeViaMaster: transferValue.routeViaMaster,
328561
- sourceAccount: transferValue.sourceAccount,
328562
- masterAccount: transferValue.masterAccount
328563
- });
328564
- } catch (error48) {
328565
- if (error48 instanceof WithdrawRoutingUnavailableError) {
328566
- log.warn("Withdraw routing unavailable, falling back", {
328567
- cex: cex3,
328568
- error: error48.message
328569
- });
328570
- if (transferValue.routeViaMaster) {
328571
- throw error48;
328572
- }
328573
- transaction = await broker.withdraw(symbol2, transferValue.amount, transferValue.recipientAddress, undefined, {
328574
- ...transferValue.params ?? {},
328575
- network: transferValue.chain
328576
- });
328577
- } else {
328578
- throw error48;
328579
- }
328580
- }
328549
+ const transaction = await broker.withdraw(symbol2, transferValue.amount, transferValue.recipientAddress, undefined, {
328550
+ ...transferValue.params ?? {},
328551
+ network: transferValue.chain
328552
+ });
328581
328553
  log.info(`Withdraw Result: ${JSON.stringify(transaction)}`);
328582
328554
  wrappedCallback(null, {
328583
328555
  proof: verityProof,
@@ -328585,10 +328557,10 @@ function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, ot
328585
328557
  });
328586
328558
  } catch (error48) {
328587
328559
  safeLogError("Withdraw failed", error48);
328588
- const message = error48 instanceof WithdrawRoutingError ? error48.message : getErrorMessage(error48);
328560
+ const code = mapCcxtErrorToGrpcStatus(error48) ?? grpc.status.INTERNAL;
328589
328561
  wrappedCallback({
328590
- code: error48 instanceof WithdrawRoutingError ? grpc.status.INVALID_ARGUMENT : grpc.status.INTERNAL,
328591
- message: `Withdraw failed: ${message}`
328562
+ code,
328563
+ message: `Withdraw failed: ${getErrorMessage(error48)}`
328592
328564
  }, null);
328593
328565
  }
328594
328566
  break;
@@ -328761,6 +328733,86 @@ function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, ot
328761
328733
  }, null);
328762
328734
  }
328763
328735
  break;
328736
+ case Action.InternalTransfer: {
328737
+ if (!symbol2) {
328738
+ return wrappedCallback({
328739
+ code: grpc.status.INVALID_ARGUMENT,
328740
+ message: `ValidationError: Symbol required`
328741
+ }, null);
328742
+ }
328743
+ const parsedPayload = parsePayload(InternalTransferPayloadSchema, call.request.payload);
328744
+ if (!parsedPayload.success) {
328745
+ return wrappedCallback({
328746
+ code: grpc.status.INVALID_ARGUMENT,
328747
+ message: parsedPayload.message
328748
+ }, null);
328749
+ }
328750
+ const transferPayload = parsedPayload.data;
328751
+ if (normalizedCex !== "binance") {
328752
+ return wrappedCallback({
328753
+ code: grpc.status.UNIMPLEMENTED,
328754
+ message: `InternalTransfer is only supported for Binance`
328755
+ }, null);
328756
+ }
328757
+ const pool = brokers[normalizedCex];
328758
+ if (!pool) {
328759
+ return wrappedCallback({
328760
+ code: grpc.status.FAILED_PRECONDITION,
328761
+ message: `No broker accounts configured for ${normalizedCex}`
328762
+ }, null);
328763
+ }
328764
+ const fromSelector = transferPayload.fromAccount ?? getCurrentBrokerSelector(metadata);
328765
+ const toSelector = transferPayload.toAccount ?? "primary";
328766
+ const sourceAccount = resolveBrokerAccount(pool, fromSelector);
328767
+ if (!sourceAccount) {
328768
+ return wrappedCallback({
328769
+ code: grpc.status.INVALID_ARGUMENT,
328770
+ message: `Source account "${fromSelector}" is not configured`
328771
+ }, null);
328772
+ }
328773
+ const destAccount = resolveBrokerAccount(pool, toSelector);
328774
+ if (!destAccount) {
328775
+ return wrappedCallback({
328776
+ code: grpc.status.INVALID_ARGUMENT,
328777
+ message: `Destination account "${toSelector}" is not configured`
328778
+ }, null);
328779
+ }
328780
+ try {
328781
+ if (useVerity) {
328782
+ sourceAccount.exchange.setHttpClientOverride(buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
328783
+ verityProof = proof;
328784
+ log.debug(`Verity proof:`, { proof, notaryPubKey });
328785
+ }), verityHttpClientOverridePredicate);
328786
+ }
328787
+ const result = await transferBinanceInternal(sourceAccount, destAccount, symbol2, transferPayload.amount);
328788
+ wrappedCallback(null, {
328789
+ proof: verityProof,
328790
+ result: JSON.stringify(result)
328791
+ });
328792
+ } catch (error48) {
328793
+ safeLogError("InternalTransfer failed", error48);
328794
+ if (error48 instanceof BrokerAccountPreconditionError) {
328795
+ return wrappedCallback({
328796
+ code: grpc.status.FAILED_PRECONDITION,
328797
+ message: getErrorMessage(error48)
328798
+ }, null);
328799
+ }
328800
+ const msg = getErrorMessage(error48);
328801
+ let code;
328802
+ if (msg.includes("Unsupported transfer direction")) {
328803
+ code = grpc.status.INVALID_ARGUMENT;
328804
+ } else if (msg.includes("unavailable in this CCXT build")) {
328805
+ code = grpc.status.UNIMPLEMENTED;
328806
+ } else {
328807
+ code = mapCcxtErrorToGrpcStatus(error48) ?? grpc.status.INTERNAL;
328808
+ }
328809
+ wrappedCallback({
328810
+ code,
328811
+ message: `InternalTransfer failed: ${msg}`
328812
+ }, null);
328813
+ }
328814
+ break;
328815
+ }
328764
328816
  default:
328765
328817
  return wrappedCallback({
328766
328818
  code: grpc.status.INVALID_ARGUMENT,
@@ -14,9 +14,8 @@ export type BrokerPoolEntry = {
14
14
  primary: BrokerAccount;
15
15
  secondaryBrokers: BrokerAccount[];
16
16
  };
17
- export declare class WithdrawRoutingError extends Error {
18
- }
19
- export declare class WithdrawRoutingUnavailableError extends WithdrawRoutingError {
17
+ export declare class BrokerAccountPreconditionError extends Error {
18
+ constructor(message: string);
20
19
  }
21
20
  export declare function authenticateRequest<T, E>(call: ServerUnaryCall<T, E>, whitelistIps: string[]): boolean;
22
21
  export declare function createVerityHttpClientOverride(verityProverUrl: string, onProofCallback: (proof: string, notaryPubKey?: string) => void): (redact: string, proofTimeout: number) => HttpClientOverride;
@@ -47,20 +46,11 @@ export declare function validateWithdraw(policy: PolicyConfig, exchange: string,
47
46
  valid: boolean;
48
47
  error?: string;
49
48
  };
50
- export declare function executeWithdrawWithRouting(args: {
51
- cex: string;
52
- brokers: BrokerPoolEntry | undefined;
53
- metadata: Metadata;
54
- selectedBroker: Exchange;
55
- code: string;
56
- amount: number;
57
- recipientAddress: string;
58
- network: string;
59
- params?: Record<string, string | number>;
60
- routeViaMaster?: boolean;
61
- sourceAccount?: string;
62
- masterAccount?: string;
63
- }): Promise<import("@usherlabs/ccxt").Transaction>;
49
+ /**
50
+ * Routes an internal transfer to the correct Binance SAPI endpoint
51
+ * based on source and destination account types.
52
+ */
53
+ export declare function transferBinanceInternal(source: BrokerAccount, dest: BrokerAccount, code: string, amount: number): Promise<unknown>;
64
54
  /**
65
55
  * Validates order request against policy rules
66
56
  */