@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.
- package/CHANGELOG.md +7 -4
- package/README.md +71 -0
- package/lib/@types/auth.d.ts +57 -2
- package/lib/@types/auth.d.ts.map +1 -1
- package/lib/@types/auth.js +2 -0
- package/lib/@types/auth.js.map +1 -1
- package/lib/@types/event.d.ts +3 -0
- 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 +2 -0
- package/lib/briij.d.ts.map +1 -1
- package/lib/briij.js +2 -0
- package/lib/briij.js.map +1 -1
- package/lib/client.d.ts +100 -7
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +469 -213
- package/lib/client.js.map +1 -1
- package/lib/wallet-recovery.d.ts +24 -0
- package/lib/wallet-recovery.d.ts.map +1 -0
- package/lib/wallet-recovery.js +256 -0
- package/lib/wallet-recovery.js.map +1 -0
- 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 +1 -1
- package/src/@types/auth.ts +61 -2
- package/src/@types/event.ts +3 -0
- package/src/@types/synapse.ts +77 -0
- package/src/briij.ts +2 -0
- package/src/client.ts +344 -46
- package/src/wallet-recovery.ts +327 -0
- package/src/xrpl/identity.ts +66 -39
- package/src/xrpl/trust.ts +35 -18
- 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
|
+
}
|
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);
|