@textrp/briij-js-sdk 42.0.0 → 43.1.0

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 (44) hide show
  1. package/CHANGELOG.md +7 -4
  2. package/README.md +71 -0
  3. package/lib/@types/auth.d.ts +57 -2
  4. package/lib/@types/auth.d.ts.map +1 -1
  5. package/lib/@types/auth.js +2 -0
  6. package/lib/@types/auth.js.map +1 -1
  7. package/lib/@types/event.d.ts +3 -0
  8. package/lib/@types/event.d.ts.map +1 -1
  9. package/lib/@types/event.js.map +1 -1
  10. package/lib/@types/synapse.d.ts +64 -0
  11. package/lib/@types/synapse.d.ts.map +1 -1
  12. package/lib/@types/synapse.js.map +1 -1
  13. package/lib/briij.d.ts +2 -0
  14. package/lib/briij.d.ts.map +1 -1
  15. package/lib/briij.js +2 -0
  16. package/lib/briij.js.map +1 -1
  17. package/lib/client.d.ts +100 -7
  18. package/lib/client.d.ts.map +1 -1
  19. package/lib/client.js +469 -213
  20. package/lib/client.js.map +1 -1
  21. package/lib/wallet-recovery.d.ts +24 -0
  22. package/lib/wallet-recovery.d.ts.map +1 -0
  23. package/lib/wallet-recovery.js +256 -0
  24. package/lib/wallet-recovery.js.map +1 -0
  25. package/lib/xrpl/identity.d.ts +2 -1
  26. package/lib/xrpl/identity.d.ts.map +1 -1
  27. package/lib/xrpl/identity.js +70 -47
  28. package/lib/xrpl/identity.js.map +1 -1
  29. package/lib/xrpl/trust.d.ts +4 -2
  30. package/lib/xrpl/trust.d.ts.map +1 -1
  31. package/lib/xrpl/trust.js +31 -19
  32. package/lib/xrpl/trust.js.map +1 -1
  33. package/lib/xrpl/verification.js +17 -6
  34. package/lib/xrpl/verification.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/@types/auth.ts +61 -2
  37. package/src/@types/event.ts +3 -0
  38. package/src/@types/synapse.ts +77 -0
  39. package/src/briij.ts +2 -0
  40. package/src/client.ts +344 -46
  41. package/src/wallet-recovery.ts +327 -0
  42. package/src/xrpl/identity.ts +66 -39
  43. package/src/xrpl/trust.ts +35 -18
  44. package/src/xrpl/verification.ts +19 -6
@@ -0,0 +1,327 @@
1
+ /*
2
+ Copyright 2026 TextRP
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { type WalletE2eeRecoveryEnvelope, type WalletRecoveryWrap } from "./@types/auth.ts";
18
+
19
+ const ENVELOPE_VERSION = 1;
20
+ const KEY_BYTES = 32;
21
+ const NONCE_BYTES = 12;
22
+ const SALT_BYTES = 16;
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";
27
+
28
+ export interface CreateDualWrapEnvelopeParams {
29
+ chainId: string;
30
+ accountId: string;
31
+ backupPassword: string;
32
+ walletWrapKey: Uint8Array;
33
+ createdAtMs?: number;
34
+ keyId?: string;
35
+ recoveryKey?: Uint8Array;
36
+ }
37
+
38
+ export interface UnwrapWithWalletParams {
39
+ envelope: WalletE2eeRecoveryEnvelope;
40
+ walletWrapKey: Uint8Array;
41
+ }
42
+
43
+ export interface UnwrapWithPasswordParams {
44
+ envelope: WalletE2eeRecoveryEnvelope;
45
+ backupPassword: string;
46
+ }
47
+
48
+ function assert(condition: boolean, message: string): asserts condition {
49
+ if (!condition) throw new Error(message);
50
+ }
51
+
52
+ function toBase64(bytes: Uint8Array): string {
53
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
54
+ let binary = "";
55
+ bytes.forEach((b) => (binary += String.fromCodePoint(b)));
56
+ return btoa(binary);
57
+ }
58
+
59
+ function fromBase64(value: string): Uint8Array {
60
+ if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64"));
61
+ const binary = atob(value);
62
+ const out = new Uint8Array(binary.length);
63
+ for (let i = 0; i < binary.length; i++) out[i] = binary.codePointAt(i)!;
64
+ return out;
65
+ }
66
+
67
+ function toUtf8(value: string): Uint8Array {
68
+ return new TextEncoder().encode(value);
69
+ }
70
+
71
+ function toArrayBuffer(value: Uint8Array): ArrayBuffer {
72
+ return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
73
+ }
74
+
75
+ function randomBytes(len: number): Uint8Array {
76
+ const out = new Uint8Array(len);
77
+ globalThis.crypto.getRandomValues(out);
78
+ return out;
79
+ }
80
+
81
+ async function importAesGcmKey(key: Uint8Array): Promise<CryptoKey> {
82
+ return await globalThis.crypto.subtle.importKey("raw", toArrayBuffer(key), { name: "AES-GCM" }, false, [
83
+ "encrypt",
84
+ "decrypt",
85
+ ]);
86
+ }
87
+
88
+ async function encryptWrap(
89
+ plaintext: Uint8Array,
90
+ {
91
+ key,
92
+ aad,
93
+ kdf,
94
+ params,
95
+ }: {
96
+ key: Uint8Array;
97
+ aad: Uint8Array;
98
+ kdf: string;
99
+ params?: Record<string, unknown>;
100
+ },
101
+ ): Promise<WalletRecoveryWrap> {
102
+ assert(key.length >= KEY_BYTES, "wallet recovery key must be at least 32 bytes");
103
+ const salt = randomBytes(SALT_BYTES);
104
+ const nonce = randomBytes(NONCE_BYTES);
105
+ const cryptoKey = await importAesGcmKey(key.slice(0, KEY_BYTES));
106
+ const ciphertext = await globalThis.crypto.subtle.encrypt(
107
+ {
108
+ name: "AES-GCM",
109
+ iv: toArrayBuffer(nonce),
110
+ additionalData: toArrayBuffer(aad),
111
+ tagLength: 128,
112
+ },
113
+ cryptoKey,
114
+ toArrayBuffer(plaintext),
115
+ );
116
+ return {
117
+ alg: "aes-256-gcm",
118
+ kdf,
119
+ salt: toBase64(salt),
120
+ nonce: toBase64(nonce),
121
+ ciphertext: toBase64(new Uint8Array(ciphertext)),
122
+ aad: toBase64(aad),
123
+ params,
124
+ };
125
+ }
126
+
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
+ );
136
+ const nonce = fromBase64(wrap.nonce);
137
+ const aad = wrap.aad ? fromBase64(wrap.aad) : new Uint8Array();
138
+ const ciphertext = fromBase64(wrap.ciphertext);
139
+ const cryptoKey = await importAesGcmKey(key.slice(0, KEY_BYTES));
140
+ const plaintext = await globalThis.crypto.subtle.decrypt(
141
+ {
142
+ name: "AES-GCM",
143
+ iv: toArrayBuffer(nonce),
144
+ additionalData: toArrayBuffer(aad),
145
+ tagLength: 128,
146
+ },
147
+ cryptoKey,
148
+ toArrayBuffer(ciphertext),
149
+ );
150
+ return new Uint8Array(plaintext);
151
+ }
152
+
153
+ async function derivePasswordWrapKey(password: string, salt: Uint8Array): Promise<Uint8Array> {
154
+ const keyMaterial = await globalThis.crypto.subtle.importKey(
155
+ "raw",
156
+ toArrayBuffer(toUtf8(password)),
157
+ "PBKDF2",
158
+ false,
159
+ ["deriveBits"],
160
+ );
161
+ const bits = await globalThis.crypto.subtle.deriveBits(
162
+ {
163
+ name: "PBKDF2",
164
+ hash: "SHA-256",
165
+ salt: toArrayBuffer(salt),
166
+ iterations: PASSWORD_KDF_ITERATIONS,
167
+ },
168
+ keyMaterial,
169
+ KEY_BYTES * 8,
170
+ );
171
+ return new Uint8Array(bits);
172
+ }
173
+
174
+ export async function deriveWalletWrapKeyFromSecret(
175
+ walletSecret: string,
176
+ chainId: string,
177
+ accountId: string,
178
+ homeserver: string,
179
+ ): Promise<Uint8Array> {
180
+ const canonicalHomeserver = canonicalizeHomeserver(homeserver);
181
+ const context = `briij-wallet-auth-wrap-v1:${canonicalHomeserver}:${chainId}:${accountId}`;
182
+ const digest = await globalThis.crypto.subtle.digest(
183
+ "SHA-256",
184
+ toArrayBuffer(toUtf8(`${context}:${walletSecret}`)),
185
+ );
186
+ return new Uint8Array(digest);
187
+ }
188
+
189
+ export function validateRecoveryEnvelopeShape(value: unknown): asserts value is WalletE2eeRecoveryEnvelope {
190
+ assert(!!value && typeof value === "object", "recovery envelope must be an object");
191
+ const envelope = value as WalletE2eeRecoveryEnvelope;
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
+ );
197
+ assert(typeof envelope.chain_id === "string" && envelope.chain_id.length > 0, "chain_id is required");
198
+ assert(typeof envelope.account_id === "string" && envelope.account_id.length > 0, "account_id is required");
199
+ assert(typeof envelope.created_at_ms === "number", "created_at_ms must be a number");
200
+ assert(typeof envelope.key_id === "string" && envelope.key_id.length > 0, "key_id is required");
201
+ assert(!!envelope.wallet_wrap && typeof envelope.wallet_wrap === "object", "wallet_wrap is required");
202
+ assert(!!envelope.password_wrap && typeof envelope.password_wrap === "object", "password_wrap is required");
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
+ );
260
+ }
261
+
262
+ export async function createDualWrapEnvelope(
263
+ params: CreateDualWrapEnvelopeParams,
264
+ ): Promise<WalletE2eeRecoveryEnvelope> {
265
+ assert(params.chainId.length > 0, "chainId is required");
266
+ assert(params.accountId.length > 0, "accountId is required");
267
+ assert(params.backupPassword.length > 0, "backupPassword is required");
268
+ assert(params.walletWrapKey.length >= KEY_BYTES, "walletWrapKey must be at least 32 bytes");
269
+
270
+ const createdAtMs = params.createdAtMs ?? Date.now();
271
+ const keyId = params.keyId ?? `k-${toBase64(randomBytes(8)).replace(/=+$/g, "")}`;
272
+ const recoveryKey = params.recoveryKey ?? randomBytes(KEY_BYTES);
273
+ assert(recoveryKey.length === KEY_BYTES, "recoveryKey must be 32 bytes");
274
+
275
+ const aad = toUtf8(`briij-recovery-envelope-v1:${params.chainId}:${params.accountId}:${keyId}:${createdAtMs}`);
276
+ const walletWrap = await encryptWrap(recoveryKey, {
277
+ key: params.walletWrapKey,
278
+ aad,
279
+ kdf: "sha256-context-v1",
280
+ });
281
+
282
+ const passwordSalt = randomBytes(SALT_BYTES);
283
+ const passwordWrapKey = await derivePasswordWrapKey(params.backupPassword, passwordSalt);
284
+ const passwordWrap = await encryptWrap(recoveryKey, {
285
+ key: passwordWrapKey,
286
+ aad,
287
+ kdf: "pbkdf2-sha256-v1",
288
+ params: {
289
+ iterations: PASSWORD_KDF_ITERATIONS,
290
+ hash: "SHA-256",
291
+ },
292
+ });
293
+ passwordWrap.salt = toBase64(passwordSalt);
294
+
295
+ return {
296
+ envelope_version: ENVELOPE_VERSION,
297
+ chain_id: params.chainId,
298
+ account_id: params.accountId,
299
+ created_at_ms: createdAtMs,
300
+ key_id: keyId,
301
+ wallet_wrap: walletWrap,
302
+ password_wrap: passwordWrap,
303
+ };
304
+ }
305
+
306
+ export async function unwrapWithWallet({ envelope, walletWrapKey }: UnwrapWithWalletParams): Promise<Uint8Array> {
307
+ validateRecoveryEnvelopeShape(envelope);
308
+ return await decryptWrap(envelope.wallet_wrap, walletWrapKey);
309
+ }
310
+
311
+ export async function unwrapWithPassword({ envelope, backupPassword }: UnwrapWithPasswordParams): Promise<Uint8Array> {
312
+ validateRecoveryEnvelopeShape(envelope);
313
+ const passwordSalt = fromBase64(envelope.password_wrap.salt);
314
+ const passwordWrapKey = await derivePasswordWrapKey(backupPassword, passwordSalt);
315
+ return await decryptWrap(envelope.password_wrap, passwordWrapKey);
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);