@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.
- package/CHANGELOG.md +7 -4
- package/lib/@types/auth.d.ts +7 -5
- package/lib/@types/auth.d.ts.map +1 -1
- package/lib/@types/auth.js.map +1 -1
- package/lib/@types/event.d.ts +3 -32
- package/lib/@types/event.d.ts.map +1 -1
- package/lib/@types/event.js.map +1 -1
- package/lib/@types/synapse.d.ts +64 -0
- package/lib/@types/synapse.d.ts.map +1 -1
- package/lib/@types/synapse.js.map +1 -1
- package/lib/briij.d.ts +1 -0
- package/lib/briij.d.ts.map +1 -1
- package/lib/briij.js +1 -0
- package/lib/briij.js.map +1 -1
- package/lib/client.d.ts +72 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +369 -196
- package/lib/client.js.map +1 -1
- package/lib/wallet-recovery.d.ts +1 -1
- package/lib/wallet-recovery.d.ts.map +1 -1
- package/lib/wallet-recovery.js +32 -8
- package/lib/wallet-recovery.js.map +1 -1
- package/lib/xrpl/identity.d.ts +2 -1
- package/lib/xrpl/identity.d.ts.map +1 -1
- package/lib/xrpl/identity.js +70 -47
- package/lib/xrpl/identity.js.map +1 -1
- package/lib/xrpl/trust.d.ts +4 -2
- package/lib/xrpl/trust.d.ts.map +1 -1
- package/lib/xrpl/trust.js +31 -19
- package/lib/xrpl/trust.js.map +1 -1
- package/lib/xrpl/verification.js +17 -6
- package/lib/xrpl/verification.js.map +1 -1
- package/package.json +127 -129
- package/src/@types/auth.ts +6 -7
- package/src/@types/event.ts +3 -32
- package/src/@types/synapse.ts +77 -0
- package/src/briij.ts +1 -0
- package/src/client.ts +261 -36
- package/src/wallet-recovery.ts +101 -26
- package/src/xrpl/identity.ts +66 -39
- package/src/xrpl/trust.ts +35 -18
- package/src/xrpl/verification.ts +19 -6
package/src/wallet-recovery.ts
CHANGED
|
@@ -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
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
147
|
-
"
|
|
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
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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(
|
|
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
|
+
}
|
package/src/xrpl/identity.ts
CHANGED
|
@@ -66,8 +66,19 @@ export function setXamanWalletForXrplIdentity(xamanWallet: XamanWalletAdapter):
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export
|
|
70
|
-
|
|
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
|
|
106
|
-
command: "tx",
|
|
107
|
-
transaction: txHash,
|
|
108
|
-
});
|
|
109
|
-
|
|
116
|
+
const txResult = await waitForValidation(xrplClient, txHash);
|
|
110
117
|
const nftTokenId =
|
|
111
|
-
|
|
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(
|
|
137
|
-
|
|
138
|
-
|
|
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 (
|
|
148
|
-
const 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(
|
|
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
|
|
213
|
-
if (!
|
|
214
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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(
|
|
35
|
-
|
|
36
|
-
|
|
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 =
|
|
42
|
+
const trustPath = config.trustPath ?? DEFAULT_TRUST_PATH;
|
|
43
43
|
const query = new URLSearchParams({
|
|
44
44
|
payer,
|
|
45
45
|
payee,
|
|
46
46
|
});
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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}`);
|
package/src/xrpl/verification.ts
CHANGED
|
@@ -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:
|
|
118
|
-
accepter_xrpl_address:
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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);
|