@fairblock/stabletrust 2.0.13 → 2.0.14

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": "@fairblock/stabletrust",
3
- "version": "2.0.13",
3
+ "version": "2.0.14",
4
4
  "description": "SDK for confidential transfers using homomorphic encryption and zero-knowledge proofs",
5
5
  "main": "src/index.js",
6
6
  "types": "src/withdrawal-fees.d.ts",
@@ -49,7 +49,7 @@
49
49
  "@stablelib/chacha20poly1305": "^1.0.1",
50
50
  "axios": "^1.16.0",
51
51
  "dotenv": "^17.2.3",
52
- "ethers": "^6.16.0"
52
+ "ethers": "^6.17.0"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=18.0.0"
package/src/backend.js CHANGED
@@ -2,6 +2,12 @@ import axios from "axios";
2
2
 
3
3
  const DEFAULT_API_BASE_URL = "https://stabletrust-backend-api.fairblock.network";
4
4
 
5
+ // Bound each request-row POST. Callers await these,
6
+ // so without a timeout a slow/hung backend would stall the caller's transfer()
7
+ // even though the on-chain transfer already completed. On timeout axios rejects
8
+ // with ECONNABORTED, which _postRequest turns into a warning.
9
+ const REQUEST_TIMEOUT_MS = 5000;
10
+
5
11
  export const CHAIN_KEY_BY_ID = {
6
12
  2201: "stable",
7
13
  5042002: "arc",
@@ -16,5 +22,7 @@ export const CHAIN_KEY_BY_ID = {
16
22
  * POST /api/requests — create a request row
17
23
  */
18
24
  export async function createRequest(requestData, apiBaseUrl = DEFAULT_API_BASE_URL) {
19
- await axios.post(`${apiBaseUrl}/api/requests`, requestData);
25
+ await axios.post(`${apiBaseUrl}/api/requests`, requestData, {
26
+ timeout: REQUEST_TIMEOUT_MS,
27
+ });
20
28
  }
@@ -121,7 +121,10 @@ export class ConfidentialTransferClient {
121
121
  }
122
122
 
123
123
  /**
124
- * Fire-and-forget request row creation
124
+ * Post a request row. Returns the createRequest promise (which resolves even
125
+ * on failure - errors are logged and swallowed) so callers that need the row
126
+ * to exist before returning (e.g. confidentialTransfer) can `await` it.
127
+ * Callers that don't await keep the old fire-and-forget behavior.
125
128
  * @private
126
129
  */
127
130
  _postRequest(data) {
@@ -132,7 +135,7 @@ export class ConfidentialTransferClient {
132
135
  applicationName: this.config.applicationName,
133
136
  ...data,
134
137
  };
135
- createRequest(payload, apiBaseUrl).catch((err) => {
138
+ return createRequest(payload, apiBaseUrl).catch((err) => {
136
139
  const status = err?.response?.status;
137
140
  const body = err?.response?.data;
138
141
  console.warn("[StableTrust] Failed to post request:", err.message);
@@ -882,67 +885,72 @@ export class ConfidentialTransferClient {
882
885
  ]);
883
886
  const tokenSymbol = await tokenContract.symbol();
884
887
 
885
- Promise.all([
886
- encryptRandomness(senderAddress, proof.data.sender_randomness),
887
- encryptRandomness(recipientAddress, proof.data.receiver_randomness),
888
- ])
889
- .then(([senderEnc, receiverEnc]) => {
890
- const transferDetails = {
891
- sender: senderAddress.toLowerCase(),
892
- recipient: recipientAddress.toLowerCase(),
893
- token: tokenSymbol,
894
- tokenAddress,
895
- };
896
-
897
- const queuedFields =
898
- senderEnc.queuedEncrypted && receiverEnc.queuedEncrypted
899
- ? {
900
- queuedEncryptedSenderRandomness: senderEnc.queuedEncrypted,
901
- queuedEncryptedSenderRandomnessId:
902
- senderEnc.queuedEncryptedId,
903
- queuedEncryptedReceiverRandomness:
904
- receiverEnc.queuedEncrypted,
905
- queuedEncryptedReceiverRandomnessId:
906
- receiverEnc.queuedEncryptedId,
907
- }
908
- : {};
909
-
910
- this._postRequest({
911
- requestId,
912
- userAddress: senderAddress.toLowerCase(),
913
- operationKind: 2,
914
- type: "transfer",
915
- transactionHash: txHash,
916
- status: "pending",
917
- details: transferDetails,
918
- encryptedSenderRandomness: senderEnc.encrypted,
919
- encryptedSenderRandomnessId: senderEnc.encryptedId,
920
- encryptedReceiverRandomness: receiverEnc.encrypted,
921
- encryptedReceiverRandomnessId: receiverEnc.encryptedId,
922
- ...queuedFields,
923
- });
924
-
925
- this._postRequest({
926
- requestId: `received-${requestId}-${recipientAddress.toLowerCase()}`,
927
- userAddress: recipientAddress.toLowerCase(),
928
- operationKind: 2,
929
- type: "received",
930
- transactionHash: txHash,
931
- status: "pending",
932
- details: transferDetails,
933
- encryptedReceiverRandomness: receiverEnc.encrypted,
934
- encryptedReceiverRandomnessId: receiverEnc.encryptedId,
935
- ...(receiverEnc.queuedEncrypted
936
- ? {
937
- queuedEncryptedReceiverRandomness:
938
- receiverEnc.queuedEncrypted,
939
- queuedEncryptedReceiverRandomnessId:
940
- receiverEnc.queuedEncryptedId,
941
- }
942
- : {}),
943
- });
944
- })
945
- .catch(() => {});
888
+ let senderEnc = null;
889
+ let receiverEnc = null;
890
+ try {
891
+ [senderEnc, receiverEnc] = await Promise.all([
892
+ encryptRandomness(senderAddress, proof.data.sender_randomness),
893
+ encryptRandomness(recipientAddress, proof.data.receiver_randomness),
894
+ ]);
895
+ } catch (encErr) {
896
+ console.warn(
897
+ `[StableTrust] Transfer ${txHash} Failed to get IBE Randomness (${encErr?.message ?? encErr}). `,
898
+ );
899
+ }
900
+
901
+ if (senderEnc && receiverEnc) {
902
+ const transferDetails = {
903
+ sender: senderAddress.toLowerCase(),
904
+ recipient: recipientAddress.toLowerCase(),
905
+ token: tokenSymbol,
906
+ tokenAddress,
907
+ };
908
+
909
+ const queuedFields =
910
+ senderEnc.queuedEncrypted && receiverEnc.queuedEncrypted
911
+ ? {
912
+ queuedEncryptedSenderRandomness: senderEnc.queuedEncrypted,
913
+ queuedEncryptedSenderRandomnessId: senderEnc.queuedEncryptedId,
914
+ queuedEncryptedReceiverRandomness: receiverEnc.queuedEncrypted,
915
+ queuedEncryptedReceiverRandomnessId:
916
+ receiverEnc.queuedEncryptedId,
917
+ }
918
+ : {};
919
+
920
+ await this._postRequest({
921
+ requestId,
922
+ userAddress: senderAddress.toLowerCase(),
923
+ operationKind: 2,
924
+ type: "transfer",
925
+ transactionHash: txHash,
926
+ status: "pending",
927
+ details: transferDetails,
928
+ encryptedSenderRandomness: senderEnc.encrypted,
929
+ encryptedSenderRandomnessId: senderEnc.encryptedId,
930
+ encryptedReceiverRandomness: receiverEnc.encrypted,
931
+ encryptedReceiverRandomnessId: receiverEnc.encryptedId,
932
+ ...queuedFields,
933
+ });
934
+
935
+ await this._postRequest({
936
+ requestId: `received-${requestId}-${recipientAddress.toLowerCase()}`,
937
+ userAddress: recipientAddress.toLowerCase(),
938
+ operationKind: 2,
939
+ type: "received",
940
+ transactionHash: txHash,
941
+ status: "pending",
942
+ details: transferDetails,
943
+ encryptedReceiverRandomness: receiverEnc.encrypted,
944
+ encryptedReceiverRandomnessId: receiverEnc.encryptedId,
945
+ ...(receiverEnc.queuedEncrypted
946
+ ? {
947
+ queuedEncryptedReceiverRandomness: receiverEnc.queuedEncrypted,
948
+ queuedEncryptedReceiverRandomnessId:
949
+ receiverEnc.queuedEncryptedId,
950
+ }
951
+ : {}),
952
+ });
953
+ }
946
954
 
947
955
  if (waitForFinalization) {
948
956
  await this._waitForGlobalState(senderAddress, "transfer");
package/src/ibe.js CHANGED
@@ -3,6 +3,14 @@ import { timelockEncrypt } from "./vendor/ts-ibe/index.js";
3
3
  const IBE_PUBKEY_URL =
4
4
  "https://anon-testnet-api.fairblock.network/fairyring/keyshare/pubkey";
5
5
 
6
+ // Bound the IBE public-key fetch. Without this, a slow or hung endpoint (a
7
+ // socket that accepts the connection but never responds) would block the fetch
8
+ // indefinitely, and because confidentialTransfer awaits encryptRandomness that
9
+ // stall propagates to the caller's transfer() call even though the on-chain
10
+ // transfer already happened. On timeout the fetch aborts and encryptRandomness
11
+ // rejects, which the caller handles as "dashboard record skipped".
12
+ const IBE_FETCH_TIMEOUT_MS = 5000;
13
+
6
14
  /** @type {{ active: string, queued: string | null } | null} */
7
15
  let cachedIbePublicKeys = null;
8
16
 
@@ -23,9 +31,22 @@ function buildIbeId(userAddress, publicKey) {
23
31
  async function getIbePublicKeys() {
24
32
  if (cachedIbePublicKeys) return cachedIbePublicKeys;
25
33
 
26
- const response = await fetch(IBE_PUBKEY_URL, {
27
- headers: { Accept: "application/json" },
28
- });
34
+ let response;
35
+ try {
36
+ response = await fetch(IBE_PUBKEY_URL, {
37
+ headers: { Accept: "application/json" },
38
+ signal: AbortSignal.timeout(IBE_FETCH_TIMEOUT_MS),
39
+ });
40
+ } catch (err) {
41
+ if (err?.name === "TimeoutError" || err?.name === "AbortError") {
42
+ throw new Error(
43
+ `IBE public key fetch timed out after ${IBE_FETCH_TIMEOUT_MS}ms`,
44
+ );
45
+ }
46
+ throw new Error(
47
+ `IBE public key fetch failed: ${err?.message ?? err}`,
48
+ );
49
+ }
29
50
 
30
51
  if (!response.ok) {
31
52
  throw new Error(