@fairblock/stabletrust 2.0.12 → 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.12",
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",
@@ -9,11 +15,14 @@ export const CHAIN_KEY_BY_ID = {
9
15
  11155111: "ethereumSepolia",
10
16
  421614: "arbitrumSepolia",
11
17
  42431: "tempo",
18
+ 42161: "arbitrumOne",
12
19
  };
13
20
 
14
21
  /**
15
22
  * POST /api/requests — create a request row
16
23
  */
17
24
  export async function createRequest(requestData, apiBaseUrl = DEFAULT_API_BASE_URL) {
18
- await axios.post(`${apiBaseUrl}/api/requests`, requestData);
25
+ await axios.post(`${apiBaseUrl}/api/requests`, requestData, {
26
+ timeout: REQUEST_TIMEOUT_MS,
27
+ });
19
28
  }
@@ -9,6 +9,7 @@ import {
9
9
  ANONYMOUS_IPFS_TRANSFER_FEE_SIGNATURE,
10
10
  ANONYMOUS_INLINE_TRANSFER_FEE_SIGNATURE,
11
11
  getStabletrustContractAddress,
12
+ STABLETRUST_CONTRACTS_BY_CHAIN_ID,
12
13
  } from "./constants.js";
13
14
  import { deriveKeys, decryptCiphertext, combineCiphertext } from "./crypto.js";
14
15
  import {
@@ -76,9 +77,9 @@ export class ConfidentialTransferClient {
76
77
  throw new Error("chainId is required");
77
78
  }
78
79
  if (!resolvedContractAddress) {
79
- const supportedChainIds = [2201, 5042002, 84532, 11155111, 421614, 42431]
80
- .map(String)
81
- .join(", ");
80
+ const supportedChainIds = Object.keys(
81
+ STABLETRUST_CONTRACTS_BY_CHAIN_ID,
82
+ ).join(", ");
82
83
  throw new Error(
83
84
  `contractAddress is required for chainId ${resolvedChainId}. No default Stabletrust contract is configured for this chain. Supported chainIds: ${supportedChainIds}`,
84
85
  );
@@ -120,7 +121,10 @@ export class ConfidentialTransferClient {
120
121
  }
121
122
 
122
123
  /**
123
- * 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.
124
128
  * @private
125
129
  */
126
130
  _postRequest(data) {
@@ -131,7 +135,7 @@ export class ConfidentialTransferClient {
131
135
  applicationName: this.config.applicationName,
132
136
  ...data,
133
137
  };
134
- createRequest(payload, apiBaseUrl).catch((err) => {
138
+ return createRequest(payload, apiBaseUrl).catch((err) => {
135
139
  const status = err?.response?.status;
136
140
  const body = err?.response?.data;
137
141
  console.warn("[StableTrust] Failed to post request:", err.message);
@@ -881,67 +885,72 @@ export class ConfidentialTransferClient {
881
885
  ]);
882
886
  const tokenSymbol = await tokenContract.symbol();
883
887
 
884
- Promise.all([
885
- encryptRandomness(senderAddress, proof.data.sender_randomness),
886
- encryptRandomness(recipientAddress, proof.data.receiver_randomness),
887
- ])
888
- .then(([senderEnc, receiverEnc]) => {
889
- const transferDetails = {
890
- sender: senderAddress.toLowerCase(),
891
- recipient: recipientAddress.toLowerCase(),
892
- token: tokenSymbol,
893
- tokenAddress,
894
- };
895
-
896
- const queuedFields =
897
- senderEnc.queuedEncrypted && receiverEnc.queuedEncrypted
898
- ? {
899
- queuedEncryptedSenderRandomness: senderEnc.queuedEncrypted,
900
- queuedEncryptedSenderRandomnessId:
901
- senderEnc.queuedEncryptedId,
902
- queuedEncryptedReceiverRandomness:
903
- receiverEnc.queuedEncrypted,
904
- queuedEncryptedReceiverRandomnessId:
905
- receiverEnc.queuedEncryptedId,
906
- }
907
- : {};
908
-
909
- this._postRequest({
910
- requestId,
911
- userAddress: senderAddress.toLowerCase(),
912
- operationKind: 2,
913
- type: "transfer",
914
- transactionHash: txHash,
915
- status: "pending",
916
- details: transferDetails,
917
- encryptedSenderRandomness: senderEnc.encrypted,
918
- encryptedSenderRandomnessId: senderEnc.encryptedId,
919
- encryptedReceiverRandomness: receiverEnc.encrypted,
920
- encryptedReceiverRandomnessId: receiverEnc.encryptedId,
921
- ...queuedFields,
922
- });
923
-
924
- this._postRequest({
925
- requestId: `received-${requestId}-${recipientAddress.toLowerCase()}`,
926
- userAddress: recipientAddress.toLowerCase(),
927
- operationKind: 2,
928
- type: "received",
929
- transactionHash: txHash,
930
- status: "pending",
931
- details: transferDetails,
932
- encryptedReceiverRandomness: receiverEnc.encrypted,
933
- encryptedReceiverRandomnessId: receiverEnc.encryptedId,
934
- ...(receiverEnc.queuedEncrypted
935
- ? {
936
- queuedEncryptedReceiverRandomness:
937
- receiverEnc.queuedEncrypted,
938
- queuedEncryptedReceiverRandomnessId:
939
- receiverEnc.queuedEncryptedId,
940
- }
941
- : {}),
942
- });
943
- })
944
- .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
+ }
945
954
 
946
955
  if (waitForFinalization) {
947
956
  await this._waitForGlobalState(senderAddress, "transfer");
package/src/constants.js CHANGED
@@ -69,6 +69,7 @@ export const STABLETRUST_CONTRACTS_BY_CHAIN_ID = Object.freeze({
69
69
  11155111: "0x7507a13352AFAa79D33E994f86f2f62463ba8DE4", //Ethereum
70
70
  421614: "0x5acECCdeb5CbD3C727eCB49F8706Eb80EF2f977F", //Arbitrum
71
71
  42431: "0xE559fB936C69c46E216bf61B07C16bF1a6d444aa", //Tempo
72
+ 42161: "0xCAA6384D5Ac8b9111D482dd676BB890f2b6e9513", //Arbitrum One (mainnet)
72
73
  });
73
74
 
74
75
  export function getStabletrustContractAddress(chainId) {
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(