@textrp/briij-js-sdk 43.0.0 → 43.1.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +7 -4
  2. package/lib/@types/auth.d.ts +7 -5
  3. package/lib/@types/auth.d.ts.map +1 -1
  4. package/lib/@types/auth.js.map +1 -1
  5. package/lib/@types/event.d.ts +3 -32
  6. package/lib/@types/event.d.ts.map +1 -1
  7. package/lib/@types/event.js.map +1 -1
  8. package/lib/@types/synapse.d.ts +64 -0
  9. package/lib/@types/synapse.d.ts.map +1 -1
  10. package/lib/@types/synapse.js.map +1 -1
  11. package/lib/briij.d.ts +1 -0
  12. package/lib/briij.d.ts.map +1 -1
  13. package/lib/briij.js +1 -0
  14. package/lib/briij.js.map +1 -1
  15. package/lib/client.d.ts +72 -1
  16. package/lib/client.d.ts.map +1 -1
  17. package/lib/client.js +369 -196
  18. package/lib/client.js.map +1 -1
  19. package/lib/wallet-recovery.d.ts +1 -1
  20. package/lib/wallet-recovery.d.ts.map +1 -1
  21. package/lib/wallet-recovery.js +32 -8
  22. package/lib/wallet-recovery.js.map +1 -1
  23. package/lib/xrpl/identity.d.ts +2 -1
  24. package/lib/xrpl/identity.d.ts.map +1 -1
  25. package/lib/xrpl/identity.js +70 -47
  26. package/lib/xrpl/identity.js.map +1 -1
  27. package/lib/xrpl/trust.d.ts +4 -2
  28. package/lib/xrpl/trust.d.ts.map +1 -1
  29. package/lib/xrpl/trust.js +31 -19
  30. package/lib/xrpl/trust.js.map +1 -1
  31. package/lib/xrpl/verification.js +17 -6
  32. package/lib/xrpl/verification.js.map +1 -1
  33. package/package.json +127 -129
  34. package/src/@types/auth.ts +6 -7
  35. package/src/@types/event.ts +3 -32
  36. package/src/@types/synapse.ts +77 -0
  37. package/src/briij.ts +1 -0
  38. package/src/client.ts +261 -36
  39. package/src/wallet-recovery.ts +101 -26
  40. package/src/xrpl/identity.ts +66 -39
  41. package/src/xrpl/trust.ts +35 -18
  42. package/src/xrpl/verification.ts +19 -6
@@ -21,6 +21,9 @@ const KEY_BYTES = 32;
21
21
  const NONCE_BYTES = 12;
22
22
  const SALT_BYTES = 16;
23
23
  const PASSWORD_KDF_ITERATIONS = 210_000;
24
+ const SUPPORTED_WRAP_ALG = "aes-256-gcm";
25
+ const SUPPORTED_WALLET_WRAP_KDF = "sha256-context-v1";
26
+ const SUPPORTED_PASSWORD_WRAP_KDF = "pbkdf2-sha256-v1";
24
27
 
25
28
  export interface CreateDualWrapEnvelopeParams {
26
29
  chainId: string;
@@ -76,13 +79,10 @@ function randomBytes(len: number): Uint8Array {
76
79
  }
77
80
 
78
81
  async function importAesGcmKey(key: Uint8Array): Promise<CryptoKey> {
79
- return await globalThis.crypto.subtle.importKey(
80
- "raw",
81
- toArrayBuffer(key),
82
- { name: "AES-GCM" },
83
- false,
84
- ["encrypt", "decrypt"],
85
- );
82
+ return await globalThis.crypto.subtle.importKey("raw", toArrayBuffer(key), { name: "AES-GCM" }, false, [
83
+ "encrypt",
84
+ "decrypt",
85
+ ]);
86
86
  }
87
87
 
88
88
  async function encryptWrap(
@@ -125,6 +125,14 @@ async function encryptWrap(
125
125
  }
126
126
 
127
127
  async function decryptWrap(wrap: WalletRecoveryWrap, key: Uint8Array): Promise<Uint8Array> {
128
+ assert(
129
+ wrap.alg === SUPPORTED_WRAP_ALG,
130
+ `Unsupported recovery wrap alg '${wrap.alg}', expected '${SUPPORTED_WRAP_ALG}'`,
131
+ );
132
+ assert(
133
+ wrap.kdf === SUPPORTED_WALLET_WRAP_KDF || wrap.kdf === SUPPORTED_PASSWORD_WRAP_KDF,
134
+ `Unsupported recovery wrap kdf '${wrap.kdf}'`,
135
+ );
128
136
  const nonce = fromBase64(wrap.nonce);
129
137
  const aad = wrap.aad ? fromBase64(wrap.aad) : new Uint8Array();
130
138
  const ciphertext = fromBase64(wrap.ciphertext);
@@ -143,9 +151,13 @@ async function decryptWrap(wrap: WalletRecoveryWrap, key: Uint8Array): Promise<U
143
151
  }
144
152
 
145
153
  async function derivePasswordWrapKey(password: string, salt: Uint8Array): Promise<Uint8Array> {
146
- const keyMaterial = await globalThis.crypto.subtle.importKey("raw", toArrayBuffer(toUtf8(password)), "PBKDF2", false, [
147
- "deriveBits",
148
- ]);
154
+ const keyMaterial = await globalThis.crypto.subtle.importKey(
155
+ "raw",
156
+ toArrayBuffer(toUtf8(password)),
157
+ "PBKDF2",
158
+ false,
159
+ ["deriveBits"],
160
+ );
149
161
  const bits = await globalThis.crypto.subtle.deriveBits(
150
162
  {
151
163
  name: "PBKDF2",
@@ -165,7 +177,8 @@ export async function deriveWalletWrapKeyFromSecret(
165
177
  accountId: string,
166
178
  homeserver: string,
167
179
  ): Promise<Uint8Array> {
168
- const context = `briij-wallet-auth-wrap-v1:${homeserver}:${chainId}:${accountId}`;
180
+ const canonicalHomeserver = canonicalizeHomeserver(homeserver);
181
+ const context = `briij-wallet-auth-wrap-v1:${canonicalHomeserver}:${chainId}:${accountId}`;
169
182
  const digest = await globalThis.crypto.subtle.digest(
170
183
  "SHA-256",
171
184
  toArrayBuffer(toUtf8(`${context}:${walletSecret}`)),
@@ -173,28 +186,82 @@ export async function deriveWalletWrapKeyFromSecret(
173
186
  return new Uint8Array(digest);
174
187
  }
175
188
 
176
- export function validateRecoveryEnvelopeShape(
177
- value: unknown,
178
- ): asserts value is WalletE2eeRecoveryEnvelope {
189
+ export function validateRecoveryEnvelopeShape(value: unknown): asserts value is WalletE2eeRecoveryEnvelope {
179
190
  assert(!!value && typeof value === "object", "recovery envelope must be an object");
180
191
  const envelope = value as WalletE2eeRecoveryEnvelope;
181
192
  assert(typeof envelope.envelope_version === "number", "envelope_version must be a number");
193
+ assert(
194
+ envelope.envelope_version === ENVELOPE_VERSION,
195
+ `Unsupported envelope_version '${envelope.envelope_version}', expected '${ENVELOPE_VERSION}'`,
196
+ );
182
197
  assert(typeof envelope.chain_id === "string" && envelope.chain_id.length > 0, "chain_id is required");
183
198
  assert(typeof envelope.account_id === "string" && envelope.account_id.length > 0, "account_id is required");
184
199
  assert(typeof envelope.created_at_ms === "number", "created_at_ms must be a number");
185
200
  assert(typeof envelope.key_id === "string" && envelope.key_id.length > 0, "key_id is required");
186
201
  assert(!!envelope.wallet_wrap && typeof envelope.wallet_wrap === "object", "wallet_wrap is required");
187
202
  assert(!!envelope.password_wrap && typeof envelope.password_wrap === "object", "password_wrap is required");
188
- for (const wrap of [envelope.wallet_wrap, envelope.password_wrap]) {
189
- assert(typeof wrap.alg === "string" && wrap.alg.length > 0, "wrap.alg is required");
190
- assert(typeof wrap.kdf === "string" && wrap.kdf.length > 0, "wrap.kdf is required");
191
- assert(typeof wrap.salt === "string" && wrap.salt.length > 0, "wrap.salt is required");
192
- assert(typeof wrap.nonce === "string" && wrap.nonce.length > 0, "wrap.nonce is required");
193
- assert(typeof wrap.ciphertext === "string" && wrap.ciphertext.length > 0, "wrap.ciphertext is required");
194
- }
203
+ assert(
204
+ typeof envelope.wallet_wrap.alg === "string" && envelope.wallet_wrap.alg.length > 0,
205
+ "wallet_wrap.alg is required",
206
+ );
207
+ assert(
208
+ typeof envelope.wallet_wrap.kdf === "string" && envelope.wallet_wrap.kdf.length > 0,
209
+ "wallet_wrap.kdf is required",
210
+ );
211
+ assert(
212
+ envelope.wallet_wrap.alg === SUPPORTED_WRAP_ALG,
213
+ `Unsupported wallet_wrap.alg '${envelope.wallet_wrap.alg}', expected '${SUPPORTED_WRAP_ALG}'`,
214
+ );
215
+ assert(
216
+ envelope.wallet_wrap.kdf === SUPPORTED_WALLET_WRAP_KDF,
217
+ `Unsupported wallet_wrap.kdf '${envelope.wallet_wrap.kdf}', expected '${SUPPORTED_WALLET_WRAP_KDF}'`,
218
+ );
219
+ assert(
220
+ typeof envelope.wallet_wrap.salt === "string" && envelope.wallet_wrap.salt.length > 0,
221
+ "wallet_wrap.salt is required",
222
+ );
223
+ assert(
224
+ typeof envelope.wallet_wrap.nonce === "string" && envelope.wallet_wrap.nonce.length > 0,
225
+ "wallet_wrap.nonce is required",
226
+ );
227
+ assert(
228
+ typeof envelope.wallet_wrap.ciphertext === "string" && envelope.wallet_wrap.ciphertext.length > 0,
229
+ "wallet_wrap.ciphertext is required",
230
+ );
231
+
232
+ assert(
233
+ typeof envelope.password_wrap.alg === "string" && envelope.password_wrap.alg.length > 0,
234
+ "password_wrap.alg is required",
235
+ );
236
+ assert(
237
+ typeof envelope.password_wrap.kdf === "string" && envelope.password_wrap.kdf.length > 0,
238
+ "password_wrap.kdf is required",
239
+ );
240
+ assert(
241
+ envelope.password_wrap.alg === SUPPORTED_WRAP_ALG,
242
+ `Unsupported password_wrap.alg '${envelope.password_wrap.alg}', expected '${SUPPORTED_WRAP_ALG}'`,
243
+ );
244
+ assert(
245
+ envelope.password_wrap.kdf === SUPPORTED_PASSWORD_WRAP_KDF,
246
+ `Unsupported password_wrap.kdf '${envelope.password_wrap.kdf}', expected '${SUPPORTED_PASSWORD_WRAP_KDF}'`,
247
+ );
248
+ assert(
249
+ typeof envelope.password_wrap.salt === "string" && envelope.password_wrap.salt.length > 0,
250
+ "password_wrap.salt is required",
251
+ );
252
+ assert(
253
+ typeof envelope.password_wrap.nonce === "string" && envelope.password_wrap.nonce.length > 0,
254
+ "password_wrap.nonce is required",
255
+ );
256
+ assert(
257
+ typeof envelope.password_wrap.ciphertext === "string" && envelope.password_wrap.ciphertext.length > 0,
258
+ "password_wrap.ciphertext is required",
259
+ );
195
260
  }
196
261
 
197
- export async function createDualWrapEnvelope(params: CreateDualWrapEnvelopeParams): Promise<WalletE2eeRecoveryEnvelope> {
262
+ export async function createDualWrapEnvelope(
263
+ params: CreateDualWrapEnvelopeParams,
264
+ ): Promise<WalletE2eeRecoveryEnvelope> {
198
265
  assert(params.chainId.length > 0, "chainId is required");
199
266
  assert(params.accountId.length > 0, "accountId is required");
200
267
  assert(params.backupPassword.length > 0, "backupPassword is required");
@@ -241,12 +308,20 @@ export async function unwrapWithWallet({ envelope, walletWrapKey }: UnwrapWithWa
241
308
  return await decryptWrap(envelope.wallet_wrap, walletWrapKey);
242
309
  }
243
310
 
244
- export async function unwrapWithPassword({
245
- envelope,
246
- backupPassword,
247
- }: UnwrapWithPasswordParams): Promise<Uint8Array> {
311
+ export async function unwrapWithPassword({ envelope, backupPassword }: UnwrapWithPasswordParams): Promise<Uint8Array> {
248
312
  validateRecoveryEnvelopeShape(envelope);
249
313
  const passwordSalt = fromBase64(envelope.password_wrap.salt);
250
314
  const passwordWrapKey = await derivePasswordWrapKey(backupPassword, passwordSalt);
251
315
  return await decryptWrap(envelope.password_wrap, passwordWrapKey);
252
316
  }
317
+
318
+ function canonicalizeHomeserver(homeserver: string): string {
319
+ const trimmed = homeserver.trim().replace(/\/+$/, "");
320
+ try {
321
+ const parsed = new URL(trimmed);
322
+ parsed.hostname = parsed.hostname.toLowerCase();
323
+ return parsed.toString().replace(/\/+$/, "");
324
+ } catch {
325
+ return trimmed.toLowerCase();
326
+ }
327
+ }
@@ -66,8 +66,19 @@ export function setXamanWalletForXrplIdentity(xamanWallet: XamanWalletAdapter):
66
66
  };
67
67
  }
68
68
 
69
- export async function mintSoulboundIdentityNFT(matrixUserId: string): Promise<XrplIdentityMintResult | null> {
70
- const { homeserverBaseUrl, accessToken, xamanWallet } = mintingConfig;
69
+ export function getConfiguredXrplIdentityMintingConfig(): Partial<XrplIdentityMintingConfig> {
70
+ return { ...mintingConfig };
71
+ }
72
+
73
+ export async function mintSoulboundIdentityNFT(
74
+ matrixUserId: string,
75
+ config?: Partial<XrplIdentityMintingConfig>,
76
+ ): Promise<XrplIdentityMintResult | null> {
77
+ const effectiveConfig = {
78
+ ...mintingConfig,
79
+ ...config,
80
+ };
81
+ const { homeserverBaseUrl, accessToken, xamanWallet } = effectiveConfig;
71
82
  if (!homeserverBaseUrl || !accessToken || !xamanWallet) {
72
83
  logger.warn(
73
84
  "Skipping XRPL identity mint for %s: missing homeserver auth and/or Xaman wallet adapter",
@@ -85,10 +96,10 @@ export async function mintSoulboundIdentityNFT(matrixUserId: string): Promise<Xr
85
96
  }
86
97
 
87
98
  const xrplAddress = await xamanWallet.getAddress();
88
- const ipfsUri = await buildIpfsUri(matrixUserId, xrplAddress);
99
+ const ipfsUri = await buildIpfsUri(matrixUserId, xrplAddress, effectiveConfig);
89
100
  const encodedUri = toHex(ipfsUri);
90
101
 
91
- const xrplClient = new XrplClient(resolveXrplWebSocketUrl());
102
+ const xrplClient = new XrplClient(resolveXrplWebSocketUrl(effectiveConfig));
92
103
  await xrplClient.connect();
93
104
 
94
105
  try {
@@ -102,13 +113,9 @@ export async function mintSoulboundIdentityNFT(matrixUserId: string): Promise<Xr
102
113
 
103
114
  const autofilled = await xrplClient.autofill(mintTransaction);
104
115
  const { hash: txHash } = await xamanWallet.signAndSubmit(autofilled as unknown as Record<string, unknown>);
105
- const txResult = await xrplClient.request({
106
- command: "tx",
107
- transaction: txHash,
108
- });
109
-
116
+ const txResult = await waitForValidation(xrplClient, txHash);
110
117
  const nftTokenId =
111
- extractNftTokenId(txResult.result?.meta) ??
118
+ extractTxScopedNftTokenId(txResult.result) ??
112
119
  (await findAccountNftByUri(xrplClient, xrplAddress, encodedUri));
113
120
  if (!nftTokenId) {
114
121
  throw new Error(`Unable to resolve NFTokenID for transaction ${txHash}`);
@@ -133,9 +140,13 @@ export async function mintSoulboundIdentityNFT(matrixUserId: string): Promise<Xr
133
140
  }
134
141
  }
135
142
 
136
- async function buildIpfsUri(matrixUserId: string, xrplAddress: string): Promise<string> {
137
- if (mintingConfig.ipfsUriFactory) {
138
- return await mintingConfig.ipfsUriFactory(matrixUserId, xrplAddress);
143
+ async function buildIpfsUri(
144
+ matrixUserId: string,
145
+ xrplAddress: string,
146
+ config: Partial<XrplIdentityMintingConfig>,
147
+ ): Promise<string> {
148
+ if (config.ipfsUriFactory) {
149
+ return await config.ipfsUriFactory(matrixUserId, xrplAddress);
139
150
  }
140
151
 
141
152
  const payload = JSON.stringify({ matrixUserId, xrplAddress });
@@ -143,12 +154,26 @@ async function buildIpfsUri(matrixUserId: string, xrplAddress: string): Promise<
143
154
  return `ipfs://textrp-briij/${toHex(digest).slice(0, 46)}`;
144
155
  }
145
156
 
146
- function resolveXrplWebSocketUrl(): string {
147
- if (mintingConfig.websocketUrl) return mintingConfig.websocketUrl;
148
- const network = mintingConfig.network ?? DEFAULT_XRPL_NETWORK;
157
+ function resolveXrplWebSocketUrl(config: Partial<XrplIdentityMintingConfig>): string {
158
+ if (config.websocketUrl) return config.websocketUrl;
159
+ const network = config.network ?? DEFAULT_XRPL_NETWORK;
149
160
  return network === "mainnet" ? DEFAULT_XRPL_MAINNET_WS : DEFAULT_XRPL_TESTNET_WS;
150
161
  }
151
162
 
163
+ async function waitForValidation(xrplClient: XrplClient, txHash: string): Promise<{ result?: unknown }> {
164
+ for (let attempt = 0; attempt < 15; attempt += 1) {
165
+ const response = await xrplClient.request({
166
+ command: "tx",
167
+ transaction: txHash,
168
+ });
169
+ if (response.result?.validated === true) {
170
+ return response as unknown as { result?: unknown };
171
+ }
172
+ await sleep(1000);
173
+ }
174
+ throw new Error(`Timed out waiting for XRPL tx validation: ${txHash}`);
175
+ }
176
+
152
177
  function accountDataUrl(baseUrl: string, matrixUserId: string): string {
153
178
  const trimmedBaseUrl = baseUrl.replace(/\/+$/, "");
154
179
  return `${trimmedBaseUrl}/_matrix/client/v3/user/${encodeURIComponent(matrixUserId)}/account_data/${encodeURIComponent(XRPL_IDENTITY_ACCOUNT_DATA_TYPE)}`;
@@ -162,7 +187,7 @@ async function getIdentityAccountData(
162
187
  const response = await fetch(accountDataUrl(homeserverBaseUrl, matrixUserId), {
163
188
  method: "GET",
164
189
  headers: {
165
- Authorization: `Bearer ${accessToken}`,
190
+ "Authorization": `Bearer ${accessToken}`,
166
191
  "Content-Type": "application/json",
167
192
  },
168
193
  });
@@ -184,7 +209,7 @@ async function setIdentityAccountData(
184
209
  const response = await fetch(accountDataUrl(homeserverBaseUrl, matrixUserId), {
185
210
  method: "PUT",
186
211
  headers: {
187
- Authorization: `Bearer ${accessToken}`,
212
+ "Authorization": `Bearer ${accessToken}`,
188
213
  "Content-Type": "application/json",
189
214
  },
190
215
  body: JSON.stringify(payload),
@@ -195,7 +220,11 @@ async function setIdentityAccountData(
195
220
  }
196
221
  }
197
222
 
198
- async function findAccountNftByUri(xrplClient: XrplClient, xrplAddress: string, encodedUri: string): Promise<string | null> {
223
+ async function findAccountNftByUri(
224
+ xrplClient: XrplClient,
225
+ xrplAddress: string,
226
+ encodedUri: string,
227
+ ): Promise<string | null> {
199
228
  const response = await xrplClient.request({
200
229
  command: "account_nfts",
201
230
  account: xrplAddress,
@@ -209,28 +238,20 @@ async function findAccountNftByUri(xrplClient: XrplClient, xrplAddress: string,
209
238
  return nft?.NFTokenID ?? null;
210
239
  }
211
240
 
212
- function extractNftTokenId(meta: unknown): string | null {
213
- if (!meta || typeof meta !== "object") return null;
214
- const affectedNodes = (meta as { AffectedNodes?: unknown[] }).AffectedNodes ?? [];
215
-
216
- for (const nodeWrapper of affectedNodes) {
217
- if (!nodeWrapper || typeof nodeWrapper !== "object") continue;
218
- const [nodeValue] = Object.values(nodeWrapper as Record<string, unknown>);
219
- if (!nodeValue || typeof nodeValue !== "object") continue;
241
+ function extractTxScopedNftTokenId(result: unknown): string | null {
242
+ if (!result || typeof result !== "object") {
243
+ return null;
244
+ }
220
245
 
221
- const newFields = (nodeValue as { NewFields?: unknown }).NewFields;
222
- const finalFields = (nodeValue as { FinalFields?: unknown }).FinalFields;
223
- const tokensContainer = [newFields, finalFields].find((container) => {
224
- if (!container || typeof container !== "object") return false;
225
- return Array.isArray((container as { NFTokens?: unknown[] }).NFTokens);
226
- }) as { NFTokens?: unknown[] } | undefined;
246
+ const payload = result as Record<string, unknown>;
247
+ const directTokenId = payload["nftoken_id"];
248
+ if (typeof directTokenId === "string" && directTokenId.length > 0) {
249
+ return directTokenId;
250
+ }
227
251
 
228
- const nftokens = tokensContainer?.NFTokens ?? [];
229
- for (const tokenEntry of nftokens) {
230
- if (!tokenEntry || typeof tokenEntry !== "object") continue;
231
- const token = (tokenEntry as { NFToken?: { NFTokenID?: string } }).NFToken;
232
- if (token?.NFTokenID) return token.NFTokenID;
233
- }
252
+ const directAlternate = payload["NFTokenID"];
253
+ if (typeof directAlternate === "string" && directAlternate.length > 0) {
254
+ return directAlternate;
234
255
  }
235
256
 
236
257
  return null;
@@ -243,3 +264,9 @@ function toHex(input: string | Uint8Array): string {
243
264
  .join("")
244
265
  .toUpperCase();
245
266
  }
267
+
268
+ async function sleep(ms: number): Promise<void> {
269
+ await new Promise((resolve) => {
270
+ setTimeout(resolve, ms);
271
+ });
272
+ }
package/src/xrpl/trust.ts CHANGED
@@ -15,6 +15,7 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  const DEFAULT_TRUST_PATH = "/_matrix/client/v3/org.textrp.xrpl/trust";
18
+ const DEFAULT_TRUST_TIMEOUT_MS = 10_000;
18
19
 
19
20
  export interface XrplTrustConfig {
20
21
  homeserverBaseUrl: string;
@@ -22,35 +23,51 @@ export interface XrplTrustConfig {
22
23
  trustPath?: string;
23
24
  }
24
25
 
25
- let xrplTrustConfig: Partial<XrplTrustConfig> = {};
26
-
27
- export function configureXrplTrust(config: Partial<XrplTrustConfig>): void {
28
- xrplTrustConfig = {
29
- ...xrplTrustConfig,
30
- ...config,
31
- };
26
+ export interface XrplTrustRequestOptions {
27
+ timeoutMs?: number;
32
28
  }
33
29
 
34
- export async function getTrustScore(payer: string, payee: string): Promise<number> {
35
- const homeserverBaseUrl = xrplTrustConfig.homeserverBaseUrl;
36
- const accessToken = xrplTrustConfig.accessToken;
30
+ export async function getTrustScore(
31
+ payer: string,
32
+ payee: string,
33
+ config: XrplTrustConfig,
34
+ options?: XrplTrustRequestOptions,
35
+ ): Promise<number> {
36
+ const { homeserverBaseUrl, accessToken } = config;
37
37
  if (!homeserverBaseUrl || !accessToken) {
38
38
  throw new Error("XRPL trust config is incomplete");
39
39
  }
40
40
 
41
41
  const trimmedBaseUrl = homeserverBaseUrl.replace(/\/+$/, "");
42
- const trustPath = xrplTrustConfig.trustPath ?? DEFAULT_TRUST_PATH;
42
+ const trustPath = config.trustPath ?? DEFAULT_TRUST_PATH;
43
43
  const query = new URLSearchParams({
44
44
  payer,
45
45
  payee,
46
46
  });
47
- const response = await fetch(`${trimmedBaseUrl}${trustPath}?${query.toString()}`, {
48
- method: "GET",
49
- headers: {
50
- Authorization: `Bearer ${accessToken}`,
51
- "Content-Type": "application/json",
52
- },
53
- });
47
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TRUST_TIMEOUT_MS;
48
+ const controller = new AbortController();
49
+ const timeoutId = setTimeout(() => {
50
+ controller.abort();
51
+ }, timeoutMs);
52
+
53
+ let response: Response;
54
+ try {
55
+ response = await fetch(`${trimmedBaseUrl}${trustPath}?${query.toString()}`, {
56
+ method: "GET",
57
+ headers: {
58
+ "Authorization": `Bearer ${accessToken}`,
59
+ "Content-Type": "application/json",
60
+ },
61
+ signal: controller.signal,
62
+ });
63
+ } catch (error) {
64
+ if (error instanceof Error && error.name === "AbortError") {
65
+ throw new Error(`Timed out fetching trust score after ${timeoutMs}ms`);
66
+ }
67
+ throw error;
68
+ } finally {
69
+ clearTimeout(timeoutId);
70
+ }
54
71
 
55
72
  if (!response.ok) {
56
73
  throw new Error(`Failed to fetch trust score: HTTP ${response.status}`);
@@ -114,8 +114,8 @@ export async function acceptVerification(issuerXrplAddress: string): Promise<Xrp
114
114
 
115
115
  await matrixClient.sendEvent(roomId, XRPL_VERIFY_ACCEPT_EVENT, {
116
116
  tx_hash: txHash,
117
- issuer_xrpl_address: signerAddress,
118
- accepter_xrpl_address: issuerXrplAddress,
117
+ issuer_xrpl_address: issuerXrplAddress,
118
+ accepter_xrpl_address: signerAddress,
119
119
  nft_token_id: identity.nftTokenId,
120
120
  ipfs_uri: updatedUri,
121
121
  trust_model: NFT_TRUST_TAG,
@@ -154,10 +154,23 @@ async function signAndValidateOnXrpl(
154
154
 
155
155
  async function waitForValidation(xrplClient: XrplClient, txHash: string): Promise<void> {
156
156
  for (let attempt = 0; attempt < 15; attempt += 1) {
157
- const response = await xrplClient.request({
158
- command: "tx",
159
- transaction: txHash,
160
- });
157
+ let response: { result?: { validated?: boolean } };
158
+ try {
159
+ response = (await xrplClient.request({
160
+ command: "tx",
161
+ transaction: txHash,
162
+ })) as { result?: { validated?: boolean } };
163
+ } catch (error) {
164
+ const errorCode =
165
+ error instanceof Error && "data" in error
166
+ ? ((error as { data?: { error?: string } }).data?.error ?? (error as { error?: string }).error)
167
+ : undefined;
168
+ if (errorCode === "txnNotFound") {
169
+ await sleep(1000);
170
+ continue;
171
+ }
172
+ throw error;
173
+ }
161
174
 
162
175
  if (response.result?.validated === true) return;
163
176
  await sleep(1000);