@trusset/sdk 0.1.0 → 0.5.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/README.md +175 -171
- package/dist/base-BYEauvBZ.d.mts +111 -0
- package/dist/base-zODt0Kf5.d.ts +111 -0
- package/dist/chunk-2KHUPARH.js +2260 -0
- package/dist/chunk-3YF42MHV.mjs +678 -0
- package/dist/chunk-4MNJZAXE.mjs +2260 -0
- package/dist/chunk-7HFMHYRA.js +107 -0
- package/dist/chunk-ACXAKW5E.js +916 -0
- package/dist/chunk-AI4ZNJLR.js +678 -0
- package/dist/chunk-C7CANC53.mjs +208 -0
- package/dist/chunk-DQCX2GYT.mjs +107 -0
- package/dist/chunk-GWMT6UU7.js +208 -0
- package/dist/chunk-OQG3PEF3.mjs +916 -0
- package/dist/flow-Cz8A09rn.d.mts +171 -0
- package/dist/flow-Cz8A09rn.d.ts +171 -0
- package/dist/index-DKUSzkBN.d.ts +956 -0
- package/dist/index-Dl4U8vUM.d.mts +956 -0
- package/dist/index.d.mts +39 -448
- package/dist/index.d.ts +39 -448
- package/dist/index.js +264 -534
- package/dist/index.mjs +249 -483
- package/dist/modules/customers/index.d.mts +2 -0
- package/dist/modules/customers/index.d.ts +2 -0
- package/dist/modules/customers/index.js +17 -0
- package/dist/modules/customers/index.mjs +17 -0
- package/dist/modules/lending/index.d.mts +6768 -0
- package/dist/modules/lending/index.d.ts +6768 -0
- package/dist/modules/lending/index.js +44 -0
- package/dist/modules/lending/index.mjs +44 -0
- package/dist/modules/tokens/index.d.mts +476 -0
- package/dist/modules/tokens/index.d.ts +476 -0
- package/dist/modules/tokens/index.js +8 -0
- package/dist/modules/tokens/index.mjs +8 -0
- package/package.json +71 -35
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfirmationError,
|
|
3
|
+
InstanceChain,
|
|
4
|
+
TrussetError,
|
|
5
|
+
ValidationError,
|
|
6
|
+
attachTransactions,
|
|
7
|
+
clampInt,
|
|
8
|
+
confirmExecuted,
|
|
9
|
+
execute,
|
|
10
|
+
optionalAddress,
|
|
11
|
+
optionalHash,
|
|
12
|
+
planOf,
|
|
13
|
+
preflightChain,
|
|
14
|
+
requireAddress,
|
|
15
|
+
requireCountry,
|
|
16
|
+
requireHash,
|
|
17
|
+
requireSigner,
|
|
18
|
+
requireString,
|
|
19
|
+
signMessage,
|
|
20
|
+
signerAddress
|
|
21
|
+
} from "./chunk-3YF42MHV.mjs";
|
|
22
|
+
|
|
23
|
+
// src/modules/customers/management.ts
|
|
24
|
+
var CustomerManagement = class {
|
|
25
|
+
constructor(http) {
|
|
26
|
+
this.http = http;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* List customers for the current instance.
|
|
30
|
+
*/
|
|
31
|
+
async list(options = {}) {
|
|
32
|
+
const params = {
|
|
33
|
+
limit: clampInt(options.limit, 1, 200, 100),
|
|
34
|
+
offset: clampInt(options.offset, 0, Number.MAX_SAFE_INTEGER, 0),
|
|
35
|
+
includeArchived: options.includeArchived ? "true" : void 0,
|
|
36
|
+
search: options.search || void 0,
|
|
37
|
+
customerType: options.customerType || void 0
|
|
38
|
+
};
|
|
39
|
+
const { data, metadata } = await this.http.getWithMeta("/customers/api/manage", params);
|
|
40
|
+
const customers = Array.isArray(data) ? data : [];
|
|
41
|
+
return { customers, total: metadata.total ?? customers.length };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get a customer by wallet address, ID, externalId, or referenceKey.
|
|
45
|
+
*
|
|
46
|
+
* Note: the backend routes 20-30 character identifiers to the internal-ID
|
|
47
|
+
* lookup, so an externalId in that length range will not resolve here -
|
|
48
|
+
* use `searchByExternalId`, which handles the ambiguity.
|
|
49
|
+
*/
|
|
50
|
+
async get(identifier) {
|
|
51
|
+
requireString(identifier, "identifier");
|
|
52
|
+
return this.http.get(`/customers/api/manage/${encodeURIComponent(identifier)}`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create a new customer record.
|
|
56
|
+
*/
|
|
57
|
+
async create(input) {
|
|
58
|
+
if (input.walletAddress) optionalAddress(input.walletAddress, "walletAddress");
|
|
59
|
+
return this.http.post("/customers/api/manage", input);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Update an existing customer.
|
|
63
|
+
*/
|
|
64
|
+
async update(customerId, input) {
|
|
65
|
+
requireString(customerId, "customerId");
|
|
66
|
+
return this.http.put(`/customers/api/manage/${encodeURIComponent(customerId)}`, input);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Archive (soft-delete) a customer.
|
|
70
|
+
*/
|
|
71
|
+
async archive(customerId) {
|
|
72
|
+
requireString(customerId, "customerId");
|
|
73
|
+
return this.http.delete(`/customers/api/manage/${encodeURIComponent(customerId)}`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Link a wallet address to a customer created via referenceKey.
|
|
77
|
+
*/
|
|
78
|
+
async linkWallet(input) {
|
|
79
|
+
requireString(input.referenceKey, "referenceKey");
|
|
80
|
+
requireAddress(input.walletAddress, "walletAddress");
|
|
81
|
+
return this.http.post("/customers/api/manage/link-wallet", input);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Batch-fetch country codes for a list of wallet addresses.
|
|
85
|
+
*/
|
|
86
|
+
async batchGetCountries(addresses) {
|
|
87
|
+
if (!Array.isArray(addresses) || addresses.length === 0) {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
return this.http.post("/customers/api/manage/countries/batch", { addresses });
|
|
91
|
+
}
|
|
92
|
+
// -----------------------------------------------------------------------
|
|
93
|
+
// Linked wallets (secondary wallets on one customer)
|
|
94
|
+
// -----------------------------------------------------------------------
|
|
95
|
+
/**
|
|
96
|
+
* List all wallets of a customer: the primary plus any linked wallets.
|
|
97
|
+
*/
|
|
98
|
+
async listWallets(customerId) {
|
|
99
|
+
requireString(customerId, "customerId");
|
|
100
|
+
return this.http.get(`/customers/api/manage/${encodeURIComponent(customerId)}/wallets`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Link an additional wallet to a customer. The wallet must not be in use
|
|
104
|
+
* by any other customer in the instance.
|
|
105
|
+
*/
|
|
106
|
+
async addWallet(customerId, walletAddress, label) {
|
|
107
|
+
requireString(customerId, "customerId");
|
|
108
|
+
requireAddress(walletAddress, "walletAddress");
|
|
109
|
+
return this.http.post(`/customers/api/manage/${encodeURIComponent(customerId)}/wallets`, {
|
|
110
|
+
walletAddress,
|
|
111
|
+
label
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Remove a linked (non-primary) wallet from a customer.
|
|
116
|
+
*/
|
|
117
|
+
async removeWallet(customerId, walletAddress) {
|
|
118
|
+
requireString(customerId, "customerId");
|
|
119
|
+
requireAddress(walletAddress, "walletAddress");
|
|
120
|
+
return this.http.delete(`/customers/api/manage/${encodeURIComponent(customerId)}/wallets/${walletAddress}`);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Promote a linked wallet to primary. The previous primary becomes a
|
|
124
|
+
* linked wallet labeled "Previously primary".
|
|
125
|
+
*/
|
|
126
|
+
async promoteWallet(customerId, walletAddress) {
|
|
127
|
+
requireString(customerId, "customerId");
|
|
128
|
+
requireAddress(walletAddress, "walletAddress");
|
|
129
|
+
return this.http.post(`/customers/api/manage/${encodeURIComponent(customerId)}/wallets/${walletAddress}/promote`);
|
|
130
|
+
}
|
|
131
|
+
// -----------------------------------------------------------------------
|
|
132
|
+
// Convenience search helpers
|
|
133
|
+
// -----------------------------------------------------------------------
|
|
134
|
+
/**
|
|
135
|
+
* Search customers by name substring.
|
|
136
|
+
*/
|
|
137
|
+
async searchByName(name, options) {
|
|
138
|
+
return this.list({ ...options, search: name });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Find a customer by wallet address.
|
|
142
|
+
*/
|
|
143
|
+
async searchByWallet(walletAddress) {
|
|
144
|
+
requireAddress(walletAddress, "walletAddress");
|
|
145
|
+
return this.get(walletAddress);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Find a customer by external ID (your system's unique identifier).
|
|
149
|
+
*/
|
|
150
|
+
async searchByExternalId(externalId) {
|
|
151
|
+
requireString(externalId, "externalId");
|
|
152
|
+
try {
|
|
153
|
+
return await this.get(externalId);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (err?.statusCode !== 404) throw err;
|
|
156
|
+
const { customers } = await this.list({ search: externalId, limit: 200 });
|
|
157
|
+
const match = customers.find((c) => c.externalId === externalId);
|
|
158
|
+
if (match) return match;
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Find a customer by reference key (for pre-wallet customers).
|
|
164
|
+
*/
|
|
165
|
+
async searchByReference(referenceKey) {
|
|
166
|
+
requireString(referenceKey, "referenceKey");
|
|
167
|
+
return this.get(referenceKey);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/utils/manifest.ts
|
|
172
|
+
function manifestToVerifyInput(manifest, overrides = {}) {
|
|
173
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
174
|
+
throw new ValidationError("manifest must be the manifest.json object produced by the KYC proof tool");
|
|
175
|
+
}
|
|
176
|
+
const walletAddress = requireAddress(overrides.walletAddress ?? manifest.walletAddress, "manifest.walletAddress");
|
|
177
|
+
const kycHash = requireHash(manifest.kycHash, "manifest.kycHash");
|
|
178
|
+
const passthrough = manifest.passthrough ?? {};
|
|
179
|
+
const country = overrides.country ?? passthrough.country;
|
|
180
|
+
if (!country || typeof country !== "string") {
|
|
181
|
+
throw new ValidationError(
|
|
182
|
+
"country is required: this manifest has no passthrough.country, pass { country } as an override"
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
walletAddress,
|
|
187
|
+
country,
|
|
188
|
+
kycHash,
|
|
189
|
+
investorType: overrides.investorType ?? passthrough.investorType,
|
|
190
|
+
softExpiryDays: overrides.softExpiryDays ?? passthrough.softExpiryDays,
|
|
191
|
+
hardExpiryDays: overrides.hardExpiryDays ?? passthrough.hardExpiryDays
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/utils/register.ts
|
|
196
|
+
var MAX_ROUNDS = 6;
|
|
197
|
+
var HASH_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
198
|
+
function asRecord(value) {
|
|
199
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
200
|
+
throw new TrussetError("The API returned no plan the SDK can sign. Nothing was signed", "UNSIGNABLE_PLAN", 0);
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
function bytesOf(hash) {
|
|
205
|
+
const out = new Uint8Array(32);
|
|
206
|
+
for (let i = 0; i < 32; i++) out[i] = parseInt(hash.slice(2 + i * 2, 4 + i * 2), 16);
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
function messageOf(error) {
|
|
210
|
+
if (error && typeof error === "object" && typeof error.message === "string") {
|
|
211
|
+
return error.message.split("\n")[0].slice(0, 200);
|
|
212
|
+
}
|
|
213
|
+
return String(error);
|
|
214
|
+
}
|
|
215
|
+
function unsignable(plan) {
|
|
216
|
+
const phase = typeof plan.phase === "string" ? ` (register phase ${plan.phase})` : "";
|
|
217
|
+
return new TrussetError(`The prepared write carries no transaction the SDK can sign${phase}, so nothing was signed`, "UNSIGNABLE_PLAN", 0);
|
|
218
|
+
}
|
|
219
|
+
async function executeAfter(chain, plan, options, earlier) {
|
|
220
|
+
try {
|
|
221
|
+
return await execute(chain, plan, options);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (!earlier.some((t) => t.txHash !== null)) throw err;
|
|
224
|
+
if (err instanceof ConfirmationError) {
|
|
225
|
+
throw new ConfirmationError(`${err.message}. Earlier steps of this registration are on chain as well`, err.code === "CONFIRMATION_FAILED" ? "CONFIRMATION_FAILED" : "TRANSACTION_FAILED", [...earlier, ...err.transactions], err.txHash, err.failure);
|
|
226
|
+
}
|
|
227
|
+
throw new ConfirmationError(`${messageOf(err)}. Earlier steps of this registration are already on chain; see err.transactions for every hash sent`, "TRANSACTION_FAILED", earlier, null, err);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function runRegisterPlan(chain, options, spec) {
|
|
231
|
+
await preflightChain(chain, options);
|
|
232
|
+
const carry = {
|
|
233
|
+
signerAddress: await signerAddress(requireSigner(options.signer)),
|
|
234
|
+
claimSignatures: [...spec.seed?.claimSignatures ?? []],
|
|
235
|
+
identities: { ...spec.seed?.identities ?? {} }
|
|
236
|
+
};
|
|
237
|
+
const transactions = [];
|
|
238
|
+
const signedMessages = /* @__PURE__ */ new Set();
|
|
239
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
240
|
+
const plan = asRecord(await spec.build(carry));
|
|
241
|
+
if (plan.phase === "DEPLOY_IDENTITY") {
|
|
242
|
+
const steps = Array.isArray(plan.steps) ? plan.steps : [];
|
|
243
|
+
if (steps.length === 0) throw unsignable(plan);
|
|
244
|
+
for (const entry of steps) {
|
|
245
|
+
const step = asRecord(entry);
|
|
246
|
+
const wallet = step.walletAddress;
|
|
247
|
+
const deployment = planOf({ steps: [step] }, { confirmIndex: 0 });
|
|
248
|
+
if (typeof wallet !== "string" || !deployment || deployment.steps[0].transaction.to !== void 0) throw unsignable(plan);
|
|
249
|
+
const executed = await executeAfter(chain, deployment, options, transactions);
|
|
250
|
+
transactions.push(...executed.transactions);
|
|
251
|
+
const hash = executed.confirmHash;
|
|
252
|
+
let recorded;
|
|
253
|
+
try {
|
|
254
|
+
recorded = await spec.recordIdentity(wallet, hash);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
throw new ConfirmationError(
|
|
257
|
+
`The ONCHAINID of ${wallet} was deployed in ${hash}, but Trusset has not recorded it: ${messageOf(err)}. Record it with identity.deployOnchainId('${wallet}', { txHash: '${hash}' }), then run this call again`,
|
|
258
|
+
"CONFIRMATION_FAILED",
|
|
259
|
+
[...transactions],
|
|
260
|
+
hash,
|
|
261
|
+
err
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (typeof recorded?.identity === "string") carry.identities[wallet] = recorded.identity;
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (plan.action === "SIGN_MESSAGES") {
|
|
269
|
+
const messages = Array.isArray(plan.messages) ? plan.messages : [];
|
|
270
|
+
if (messages.length === 0) throw unsignable(plan);
|
|
271
|
+
for (const entry of messages) {
|
|
272
|
+
const message = asRecord(entry);
|
|
273
|
+
const hash = message.messageHash;
|
|
274
|
+
if (message.scheme !== void 0 && message.scheme !== "eip191" || typeof hash !== "string" || !HASH_RE.test(hash) || typeof message.walletAddress !== "string") {
|
|
275
|
+
throw new TrussetError("The register asks for a claim signature in a form the SDK does not produce. Nothing was signed", "UNSIGNABLE_MESSAGE", 0);
|
|
276
|
+
}
|
|
277
|
+
const key = hash.toLowerCase();
|
|
278
|
+
if (signedMessages.has(key)) {
|
|
279
|
+
throw new TrussetError(`The register asked again for the claim of ${message.walletAddress} the signer already signed, so the signer is not accepted as a claim key of the trusted issuer. Nothing further was signed`, "CLAIM_SIGNATURE_REFUSED", 0);
|
|
280
|
+
}
|
|
281
|
+
const signature = await signMessage(chain, options, bytesOf(hash));
|
|
282
|
+
signedMessages.add(key);
|
|
283
|
+
carry.claimSignatures.push({
|
|
284
|
+
walletAddress: message.walletAddress,
|
|
285
|
+
signature,
|
|
286
|
+
...typeof message.claimType === "string" ? { claimType: message.claimType } : {}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const resolved = planOf(plan, spec.plan);
|
|
292
|
+
if (!resolved) throw unsignable(plan);
|
|
293
|
+
const final = await executeAfter(chain, resolved, options, transactions);
|
|
294
|
+
return { plan, steps: resolved.steps, final, transactions: [...transactions, ...final.transactions] };
|
|
295
|
+
}
|
|
296
|
+
throw new TrussetError(`The register plan did not settle into transactions after ${MAX_ROUNDS} rounds; nothing further was signed`, "PLAN_DID_NOT_SETTLE", 0);
|
|
297
|
+
}
|
|
298
|
+
async function settle(run, hash, retryWith, confirm) {
|
|
299
|
+
try {
|
|
300
|
+
const result = await confirm();
|
|
301
|
+
const base = result && typeof result === "object" ? result : { result };
|
|
302
|
+
return { ...base, transactions: run.transactions };
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw new ConfirmationError(
|
|
305
|
+
`${hash ? `Transaction ${hash} is` : "The signed transactions are"} on chain but Trusset has not recorded ${hash ? "it" : "them"}: ${messageOf(err)}. Confirm again with ${retryWith}`,
|
|
306
|
+
"CONFIRMATION_FAILED",
|
|
307
|
+
run.transactions,
|
|
308
|
+
hash,
|
|
309
|
+
err
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function confirmRun(run, confirm) {
|
|
314
|
+
const hash = run.final.confirmHash;
|
|
315
|
+
if (!hash) {
|
|
316
|
+
throw new ConfirmationError("The plan finished without a transaction to confirm. See err.transactions", "CONFIRMATION_FAILED", run.transactions, null, null);
|
|
317
|
+
}
|
|
318
|
+
return settle(run, hash, `{ txHash: '${hash}' }`, () => confirm(hash));
|
|
319
|
+
}
|
|
320
|
+
async function confirmRunSteps(run, confirm, retryWith) {
|
|
321
|
+
if (!run.final.transactions.some((t) => t.txHash !== null)) {
|
|
322
|
+
throw new ConfirmationError("The plan finished without a transaction to confirm. See err.transactions", "CONFIRMATION_FAILED", run.transactions, null, null);
|
|
323
|
+
}
|
|
324
|
+
return settle(run, null, retryWith, confirm);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/modules/customers/identity.ts
|
|
328
|
+
var ONCHAIN_WRITE_TIMEOUT_MS = 6e4;
|
|
329
|
+
var PROOF_CLAIMS_TIMEOUT_MS = 18e4;
|
|
330
|
+
var BUILD = { timeoutMs: ONCHAIN_WRITE_TIMEOUT_MS };
|
|
331
|
+
var CONFIRM = BUILD;
|
|
332
|
+
var PROOF_BUILD = { timeoutMs: PROOF_CLAIMS_TIMEOUT_MS };
|
|
333
|
+
var PROOF_CONFIRM = PROOF_BUILD;
|
|
334
|
+
var SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
335
|
+
var BASE64_SIG_RE = /^[A-Za-z0-9+/]{86}(==)?$/;
|
|
336
|
+
var HEX_SIG_RE = /^(0x)?[0-9a-fA-F]{128}$/;
|
|
337
|
+
function hasSigner(options) {
|
|
338
|
+
return Boolean(options && typeof options === "object" && "signer" in options);
|
|
339
|
+
}
|
|
340
|
+
function hasTxHash(options) {
|
|
341
|
+
return Boolean(options && typeof options === "object" && "txHash" in options && !("signer" in options));
|
|
342
|
+
}
|
|
343
|
+
function planFields(input) {
|
|
344
|
+
const out = {};
|
|
345
|
+
if (!input) return out;
|
|
346
|
+
const signerAddress2 = optionalAddress(input.signerAddress, "signerAddress");
|
|
347
|
+
if (signerAddress2) out.signerAddress = signerAddress2;
|
|
348
|
+
if (input.claimSignatures !== void 0) {
|
|
349
|
+
if (!Array.isArray(input.claimSignatures)) throw new ValidationError("claimSignatures must be a list of { walletAddress, signature, claimType }");
|
|
350
|
+
out.claimSignatures = input.claimSignatures.map((entry) => {
|
|
351
|
+
requireAddress(entry?.walletAddress, "claimSignatures[].walletAddress");
|
|
352
|
+
if (typeof entry.signature !== "string" || !SIGNATURE_RE.test(entry.signature)) {
|
|
353
|
+
throw new ValidationError("claimSignatures[].signature must be a 65-byte hex signature (0x + 130 hex chars)");
|
|
354
|
+
}
|
|
355
|
+
return entry.claimType === void 0 ? { walletAddress: entry.walletAddress, signature: entry.signature } : { walletAddress: entry.walletAddress, signature: entry.signature, claimType: entry.claimType };
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (input.identities !== void 0) {
|
|
359
|
+
if (!input.identities || typeof input.identities !== "object" || Array.isArray(input.identities)) {
|
|
360
|
+
throw new ValidationError("identities must map each wallet address to its ONCHAINID address");
|
|
361
|
+
}
|
|
362
|
+
const identities = {};
|
|
363
|
+
for (const [wallet, identity] of Object.entries(input.identities)) {
|
|
364
|
+
identities[requireAddress(wallet, "identities key")] = requireAddress(identity, `identities[${wallet}]`);
|
|
365
|
+
}
|
|
366
|
+
out.identities = identities;
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
function withoutPlanFields(input) {
|
|
371
|
+
const rest = { ...input };
|
|
372
|
+
delete rest.signerAddress;
|
|
373
|
+
delete rest.claimSignatures;
|
|
374
|
+
delete rest.identities;
|
|
375
|
+
return rest;
|
|
376
|
+
}
|
|
377
|
+
function bytesToBase64(bytes) {
|
|
378
|
+
let binary = "";
|
|
379
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
380
|
+
return btoa(binary);
|
|
381
|
+
}
|
|
382
|
+
function hexToBytes(hex) {
|
|
383
|
+
const body = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
384
|
+
const out = new Uint8Array(body.length / 2);
|
|
385
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(body.slice(i * 2, i * 2 + 2), 16);
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
function manifestSigOf(value) {
|
|
389
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
390
|
+
const hint = "manifestSig must be the bundle's 64-byte manifest.sig: pass readFileSync(path) or readFileSync(path).toString('base64'), not the file read as text";
|
|
391
|
+
if (value instanceof Uint8Array) {
|
|
392
|
+
if (value.length !== 64) throw new ValidationError(`${hint}. These bytes are ${value.length} long`);
|
|
393
|
+
return bytesToBase64(value);
|
|
394
|
+
}
|
|
395
|
+
if (typeof value !== "string") throw new ValidationError(hint);
|
|
396
|
+
const s = value.trim();
|
|
397
|
+
if (BASE64_SIG_RE.test(s)) return s;
|
|
398
|
+
if (HEX_SIG_RE.test(s)) return bytesToBase64(hexToBytes(s));
|
|
399
|
+
throw new ValidationError(hint);
|
|
400
|
+
}
|
|
401
|
+
function proofBody(input) {
|
|
402
|
+
const { manifestSig, ...rest } = withoutPlanFields(input);
|
|
403
|
+
const sig = manifestSigOf(manifestSig);
|
|
404
|
+
return sig === void 0 ? rest : { ...rest, manifestSig: sig };
|
|
405
|
+
}
|
|
406
|
+
var CustomerIdentity = class {
|
|
407
|
+
constructor(http, chain) {
|
|
408
|
+
this.http = http;
|
|
409
|
+
this.chain = chain ?? new InstanceChain(http);
|
|
410
|
+
}
|
|
411
|
+
async verify(input, options) {
|
|
412
|
+
requireAddress(input.walletAddress, "walletAddress");
|
|
413
|
+
requireCountry(input.country, "country");
|
|
414
|
+
optionalHash(input.kycHash, "kycHash");
|
|
415
|
+
const plan = planFields(input);
|
|
416
|
+
const base = withoutPlanFields(input);
|
|
417
|
+
if (hasTxHash(options)) {
|
|
418
|
+
requireHash(options.txHash, "txHash");
|
|
419
|
+
return this.confirmVerify({ ...base, txHash: options.txHash });
|
|
420
|
+
}
|
|
421
|
+
if (!hasSigner(options)) {
|
|
422
|
+
return this.http.post("/customers/api/identity/verify", { ...base, ...plan }, BUILD);
|
|
423
|
+
}
|
|
424
|
+
let pinned = base.kycHash;
|
|
425
|
+
const run = await runRegisterPlan(this.chain, options, {
|
|
426
|
+
build: async (carry) => {
|
|
427
|
+
const prepared = await this.http.post("/customers/api/identity/verify", { ...base, ...pinned ? { kycHash: pinned } : {}, ...carry }, BUILD);
|
|
428
|
+
if (!pinned && typeof prepared?.kycHash === "string") pinned = prepared.kycHash;
|
|
429
|
+
return prepared;
|
|
430
|
+
},
|
|
431
|
+
recordIdentity: (wallet, txHash) => this.confirmOnchainId(wallet, txHash),
|
|
432
|
+
seed: plan
|
|
433
|
+
});
|
|
434
|
+
const kycHash = typeof run.plan.kycHash === "string" ? run.plan.kycHash : pinned;
|
|
435
|
+
return confirmRun(run, (txHash) => this.confirmVerify({ ...base, kycHash, txHash }));
|
|
436
|
+
}
|
|
437
|
+
async verifyFromManifest(manifest, overrides, options) {
|
|
438
|
+
const input = { ...manifestToVerifyInput(manifest, overrides), ...planFields(overrides) };
|
|
439
|
+
return options ? this.verify(input, options) : this.verify(input);
|
|
440
|
+
}
|
|
441
|
+
async batchVerify(entries, options) {
|
|
442
|
+
if (!Array.isArray(entries) || entries.length === 0 || entries.length > 500) {
|
|
443
|
+
throw new ValidationError("Provide 1-500 entries for batch verification");
|
|
444
|
+
}
|
|
445
|
+
for (const e of entries) {
|
|
446
|
+
requireAddress(e.walletAddress, "walletAddress");
|
|
447
|
+
requireCountry(e.country, "country");
|
|
448
|
+
optionalHash(e.kycHash, "kycHash");
|
|
449
|
+
}
|
|
450
|
+
if (hasTxHash(options)) {
|
|
451
|
+
requireHash(options.txHash, "txHash");
|
|
452
|
+
return this.confirmBatchVerify(entries, options.txHash);
|
|
453
|
+
}
|
|
454
|
+
if (!hasSigner(options)) {
|
|
455
|
+
return this.http.post("/customers/api/identity/batch-verify", { entries, ...planFields(options) }, BUILD);
|
|
456
|
+
}
|
|
457
|
+
let pinned = entries;
|
|
458
|
+
const run = await runRegisterPlan(this.chain, options, {
|
|
459
|
+
build: async (carry) => {
|
|
460
|
+
const prepared2 = await this.http.post("/customers/api/identity/batch-verify", { entries: pinned, ...carry }, BUILD);
|
|
461
|
+
pinned = pinEntryHashes(pinned, prepared2);
|
|
462
|
+
return prepared2;
|
|
463
|
+
},
|
|
464
|
+
recordIdentity: (wallet, txHash) => this.confirmOnchainId(wallet, txHash)
|
|
465
|
+
});
|
|
466
|
+
const prepared = run.plan;
|
|
467
|
+
const merged = mergePreparedEntries(entries, prepared);
|
|
468
|
+
return confirmRun(run, async (txHash) => withPrepareRejections(await this.confirmBatchVerify(merged.entries, txHash), prepared, merged.origin));
|
|
469
|
+
}
|
|
470
|
+
async revoke(walletAddress, options) {
|
|
471
|
+
requireAddress(walletAddress, "walletAddress");
|
|
472
|
+
if (hasTxHash(options)) {
|
|
473
|
+
requireHash(options.txHash, "txHash");
|
|
474
|
+
return this.confirmRevoke(walletAddress, options.txHash);
|
|
475
|
+
}
|
|
476
|
+
if (!hasSigner(options)) {
|
|
477
|
+
return this.http.post("/customers/api/identity/revoke", { walletAddress }, BUILD);
|
|
478
|
+
}
|
|
479
|
+
const run = await runRegisterPlan(this.chain, options, {
|
|
480
|
+
build: () => this.http.post("/customers/api/identity/revoke", { walletAddress }, BUILD),
|
|
481
|
+
recordIdentity: (wallet, txHash) => this.confirmOnchainId(wallet, txHash)
|
|
482
|
+
});
|
|
483
|
+
return confirmRun(run, (txHash) => this.confirmRevoke(walletAddress, txHash));
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Get combined on-chain and off-chain identity status.
|
|
487
|
+
*/
|
|
488
|
+
async getStatus(walletAddress) {
|
|
489
|
+
requireAddress(walletAddress, "walletAddress");
|
|
490
|
+
return this.http.get(`/customers/api/identity/status/${walletAddress}`);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Check whether a token transfer between two addresses is compliant.
|
|
494
|
+
* Runs identity checks + all registered compliance modules.
|
|
495
|
+
*/
|
|
496
|
+
async canTransfer(input) {
|
|
497
|
+
requireAddress(input.from, "from");
|
|
498
|
+
requireAddress(input.to, "to");
|
|
499
|
+
return this.http.post("/customers/api/identity/can-transfer", input);
|
|
500
|
+
}
|
|
501
|
+
async deployOnchainId(walletAddress, options) {
|
|
502
|
+
requireAddress(walletAddress, "walletAddress");
|
|
503
|
+
if (hasTxHash(options)) {
|
|
504
|
+
requireHash(options.txHash, "txHash");
|
|
505
|
+
return this.confirmOnchainId(walletAddress, options.txHash);
|
|
506
|
+
}
|
|
507
|
+
if (!hasSigner(options)) {
|
|
508
|
+
return this.http.post("/customers/api/identity/onchainid", { walletAddress, ...planFields(options) }, BUILD);
|
|
509
|
+
}
|
|
510
|
+
await preflightChain(this.chain, options);
|
|
511
|
+
const signer = await signerAddress(requireSigner(options.signer));
|
|
512
|
+
const prepared = await this.http.post("/customers/api/identity/onchainid", { walletAddress, signerAddress: signer }, BUILD);
|
|
513
|
+
if (prepared.exists) return attachTransactions(prepared, []);
|
|
514
|
+
const plan = planOf(prepared, { confirmIndex: 0 });
|
|
515
|
+
if (!plan || plan.steps[0].transaction.to !== void 0) {
|
|
516
|
+
throw new TrussetError("The ONCHAINID build carries no identity deployment the SDK can sign, so nothing was signed", "UNSIGNABLE_PLAN", 0);
|
|
517
|
+
}
|
|
518
|
+
const executed = await execute(this.chain, plan, options);
|
|
519
|
+
return confirmExecuted(executed, (txHash) => this.confirmOnchainId(walletAddress, txHash));
|
|
520
|
+
}
|
|
521
|
+
// -----------------------------------------------------------------------
|
|
522
|
+
// On-chain claims
|
|
523
|
+
// -----------------------------------------------------------------------
|
|
524
|
+
/**
|
|
525
|
+
* List the active on-chain claims of a wallet.
|
|
526
|
+
*/
|
|
527
|
+
async getClaims(walletAddress) {
|
|
528
|
+
requireAddress(walletAddress, "walletAddress");
|
|
529
|
+
return this.http.get(`/customers/api/identity/claims/${walletAddress}`);
|
|
530
|
+
}
|
|
531
|
+
async addClaimsFromProof(input, options) {
|
|
532
|
+
requireAddress(input.walletAddress, "walletAddress");
|
|
533
|
+
if (!input.manifest || typeof input.manifest !== "object" || Array.isArray(input.manifest)) {
|
|
534
|
+
throw new ValidationError("manifest must be the manifest.json object produced by the KYC proof tool");
|
|
535
|
+
}
|
|
536
|
+
const plan = planFields(input);
|
|
537
|
+
const body = proofBody(input);
|
|
538
|
+
if (options && !hasSigner(options)) {
|
|
539
|
+
if (!Array.isArray(options.txResults) || options.txResults.length === 0) {
|
|
540
|
+
throw new ValidationError("txResults must list the signed claims as [{ claimType, txHash }]");
|
|
541
|
+
}
|
|
542
|
+
for (const r of options.txResults) requireHash(r?.txHash, "txResults[].txHash");
|
|
543
|
+
return this.confirmProofClaims(body, options.txResults);
|
|
544
|
+
}
|
|
545
|
+
if (!options) {
|
|
546
|
+
return this.http.post("/customers/api/identity/claims/from-proof", { ...body, ...plan }, PROOF_BUILD);
|
|
547
|
+
}
|
|
548
|
+
const run = await runRegisterPlan(this.chain, options, {
|
|
549
|
+
build: (carry) => this.http.post("/customers/api/identity/claims/from-proof", { ...body, ...carry }, PROOF_BUILD),
|
|
550
|
+
recordIdentity: (wallet, txHash) => this.confirmOnchainId(wallet, txHash),
|
|
551
|
+
seed: plan,
|
|
552
|
+
plan: { confirmIndex: null }
|
|
553
|
+
});
|
|
554
|
+
const planned = Array.isArray(run.plan.steps) ? run.plan.steps : [];
|
|
555
|
+
const txResults = [];
|
|
556
|
+
run.final.transactions.forEach((sent, i) => {
|
|
557
|
+
const claimType = planned[i]?.claimType;
|
|
558
|
+
if (claimType && sent.txHash) txResults.push({ claimType, txHash: sent.txHash });
|
|
559
|
+
});
|
|
560
|
+
return confirmRunSteps(run, () => this.confirmProofClaims(body, txResults), "{ txResults }");
|
|
561
|
+
}
|
|
562
|
+
async addClaim(input, options) {
|
|
563
|
+
requireAddress(input.walletAddress, "walletAddress");
|
|
564
|
+
if (input.claimType === void 0 || input.claimType === null) {
|
|
565
|
+
throw new ValidationError("claimType is required");
|
|
566
|
+
}
|
|
567
|
+
if (!input.dataHash && !input.dataInput) {
|
|
568
|
+
throw new ValidationError("Provide dataHash (32-byte hex) or dataInput (string) for the claim");
|
|
569
|
+
}
|
|
570
|
+
optionalHash(input.dataHash, "dataHash");
|
|
571
|
+
const plan = planFields(input);
|
|
572
|
+
const base = withoutPlanFields(input);
|
|
573
|
+
if (hasTxHash(options)) {
|
|
574
|
+
requireHash(options.txHash, "txHash");
|
|
575
|
+
return this.confirmAddClaim({ ...base, txHash: options.txHash });
|
|
576
|
+
}
|
|
577
|
+
if (!hasSigner(options)) {
|
|
578
|
+
return this.http.post("/customers/api/identity/add-claim", { ...base, ...plan }, BUILD);
|
|
579
|
+
}
|
|
580
|
+
const run = await runRegisterPlan(this.chain, options, {
|
|
581
|
+
build: (carry) => this.http.post("/customers/api/identity/add-claim", { ...base, ...carry }, BUILD),
|
|
582
|
+
recordIdentity: (wallet, txHash) => this.confirmOnchainId(wallet, txHash),
|
|
583
|
+
seed: plan
|
|
584
|
+
});
|
|
585
|
+
const dataHash = typeof run.plan.dataHash === "string" ? run.plan.dataHash : base.dataHash;
|
|
586
|
+
return confirmRun(run, (txHash) => this.confirmAddClaim({ ...base, dataHash, txHash }));
|
|
587
|
+
}
|
|
588
|
+
async revokeClaim(walletAddress, claimType, options) {
|
|
589
|
+
requireAddress(walletAddress, "walletAddress");
|
|
590
|
+
if (claimType === void 0 || claimType === null) {
|
|
591
|
+
throw new ValidationError("claimType is required");
|
|
592
|
+
}
|
|
593
|
+
if (hasTxHash(options)) {
|
|
594
|
+
requireHash(options.txHash, "txHash");
|
|
595
|
+
return this.confirmRevokeClaim(walletAddress, claimType, options.txHash);
|
|
596
|
+
}
|
|
597
|
+
if (!hasSigner(options)) {
|
|
598
|
+
return this.http.post("/customers/api/identity/revoke-claim", { walletAddress, claimType }, BUILD);
|
|
599
|
+
}
|
|
600
|
+
const run = await runRegisterPlan(this.chain, options, {
|
|
601
|
+
build: () => this.http.post("/customers/api/identity/revoke-claim", { walletAddress, claimType }, BUILD),
|
|
602
|
+
recordIdentity: (wallet, txHash) => this.confirmOnchainId(wallet, txHash)
|
|
603
|
+
});
|
|
604
|
+
return confirmRun(run, (txHash) => this.confirmRevokeClaim(walletAddress, claimType, txHash));
|
|
605
|
+
}
|
|
606
|
+
// -----------------------------------------------------------------------
|
|
607
|
+
// Confirmations: the backend reads the mined receipt, checks it targets the
|
|
608
|
+
// identity registry and the wallet it should, and only then records it.
|
|
609
|
+
// -----------------------------------------------------------------------
|
|
610
|
+
confirmVerify(body) {
|
|
611
|
+
return this.http.post("/customers/api/identity/verify", body, CONFIRM);
|
|
612
|
+
}
|
|
613
|
+
confirmBatchVerify(entries, txHash) {
|
|
614
|
+
return this.http.post("/customers/api/identity/batch-verify", { entries, txHash }, CONFIRM);
|
|
615
|
+
}
|
|
616
|
+
confirmRevoke(walletAddress, txHash) {
|
|
617
|
+
return this.http.post("/customers/api/identity/revoke", { walletAddress, txHash }, CONFIRM);
|
|
618
|
+
}
|
|
619
|
+
confirmAddClaim(body) {
|
|
620
|
+
return this.http.post("/customers/api/identity/add-claim", body, CONFIRM);
|
|
621
|
+
}
|
|
622
|
+
confirmRevokeClaim(walletAddress, claimType, txHash) {
|
|
623
|
+
return this.http.post("/customers/api/identity/revoke-claim", { walletAddress, claimType, txHash }, CONFIRM);
|
|
624
|
+
}
|
|
625
|
+
confirmProofClaims(body, txResults) {
|
|
626
|
+
return this.http.post("/customers/api/identity/claims/from-proof", { ...body, txResults }, PROOF_CONFIRM);
|
|
627
|
+
}
|
|
628
|
+
confirmOnchainId(walletAddress, txHash) {
|
|
629
|
+
return this.http.post("/customers/api/identity/onchainid/confirm", { walletAddress, txHash }, CONFIRM);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
function mergePreparedEntries(entries, prepared) {
|
|
633
|
+
const accepted = Array.isArray(prepared.entries) ? prepared.entries : [];
|
|
634
|
+
const origin = [];
|
|
635
|
+
const merged = accepted.map((entry) => {
|
|
636
|
+
const index = entries.findIndex((e) => e.walletAddress.toLowerCase() === entry.walletAddress.toLowerCase());
|
|
637
|
+
origin.push(index);
|
|
638
|
+
return {
|
|
639
|
+
...index >= 0 ? entries[index] : {},
|
|
640
|
+
walletAddress: entry.walletAddress,
|
|
641
|
+
country: entry.country,
|
|
642
|
+
kycHash: entry.kycHash
|
|
643
|
+
};
|
|
644
|
+
});
|
|
645
|
+
return { entries: merged, origin };
|
|
646
|
+
}
|
|
647
|
+
function pinEntryHashes(entries, prepared) {
|
|
648
|
+
const derived = new Map((Array.isArray(prepared?.entries) ? prepared.entries : []).map((e) => [e.walletAddress.toLowerCase(), e.kycHash]));
|
|
649
|
+
return entries.map((e) => {
|
|
650
|
+
const hash = e.kycHash ?? derived.get(e.walletAddress.toLowerCase());
|
|
651
|
+
return hash ? { ...e, kycHash: hash } : e;
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
function withPrepareRejections(confirmed, prepared, origin) {
|
|
655
|
+
const early = Array.isArray(prepared.rejected) ? prepared.rejected : [];
|
|
656
|
+
const late = (Array.isArray(confirmed.rejected) ? confirmed.rejected : []).map((r) => ({ ...r, index: origin[r.index] ?? r.index }));
|
|
657
|
+
const rejected = [...early, ...late].sort((a, b) => a.index - b.index);
|
|
658
|
+
return {
|
|
659
|
+
...confirmed,
|
|
660
|
+
submitted: typeof prepared.submitted === "number" ? prepared.submitted : confirmed.submitted,
|
|
661
|
+
rejectedCount: rejected.length,
|
|
662
|
+
rejected
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/modules/customers/id-links.ts
|
|
667
|
+
var CustomerIdLinks = class {
|
|
668
|
+
constructor(http) {
|
|
669
|
+
this.http = http;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Create a KYC verification link to send to an investor.
|
|
673
|
+
*/
|
|
674
|
+
async create(input) {
|
|
675
|
+
if (!input.recipientEmail && !input.recipientWallet && !input.recipientPhone) {
|
|
676
|
+
throw new ValidationError("At least one of recipientEmail, recipientWallet, or recipientPhone is required");
|
|
677
|
+
}
|
|
678
|
+
return this.http.post("/customers/api/id-links", input);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* List all verification links for the current instance.
|
|
682
|
+
*/
|
|
683
|
+
async list(options = {}) {
|
|
684
|
+
const params = {
|
|
685
|
+
limit: clampInt(options.limit, 1, 200, 50),
|
|
686
|
+
offset: clampInt(options.offset, 0, Number.MAX_SAFE_INTEGER, 0),
|
|
687
|
+
status: options.status
|
|
688
|
+
};
|
|
689
|
+
const { data, metadata } = await this.http.getWithMeta("/customers/api/id-links", params);
|
|
690
|
+
const links = Array.isArray(data) ? data : [];
|
|
691
|
+
return { links, total: metadata.total ?? links.length };
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Revoke (expire) a verification link so it can no longer be used.
|
|
695
|
+
*/
|
|
696
|
+
async revoke(linkId) {
|
|
697
|
+
requireString(linkId, "linkId");
|
|
698
|
+
await this.http.put(`/customers/api/id-links/${encodeURIComponent(linkId)}/revoke`);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Resend a verification link via the specified channels.
|
|
702
|
+
*/
|
|
703
|
+
async resend(linkId, channels) {
|
|
704
|
+
requireString(linkId, "linkId");
|
|
705
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
706
|
+
throw new ValidationError("channels array is required (email and/or sms)");
|
|
707
|
+
}
|
|
708
|
+
return this.http.post(`/customers/api/id-links/${encodeURIComponent(linkId)}/resend`, { channels });
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// src/modules/customers/sync.ts
|
|
713
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
714
|
+
var MAX_BATCH_SIZE = 200;
|
|
715
|
+
var MAX_VERIFY_BATCH = 500;
|
|
716
|
+
var CustomerSync = class {
|
|
717
|
+
constructor(http, identity) {
|
|
718
|
+
this.http = http;
|
|
719
|
+
this.manage = new CustomerManagement(http);
|
|
720
|
+
this.identity = identity ?? new CustomerIdentity(http);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Import customer records from an external system.
|
|
724
|
+
*
|
|
725
|
+
* Each record is mapped by `externalId`. If a customer with that externalId
|
|
726
|
+
* already exists, the `onConflict` strategy determines whether to skip or
|
|
727
|
+
* update.
|
|
728
|
+
*
|
|
729
|
+
* When `verifyOnChain` is true, records that include a `walletAddress` and
|
|
730
|
+
* `country` are batch-verified on-chain after the database import. That
|
|
731
|
+
* needs `signer`: verification transactions are signed by your wallet, not
|
|
732
|
+
* by Trusset.
|
|
733
|
+
*/
|
|
734
|
+
async importRecords(records, options = {}) {
|
|
735
|
+
if (!Array.isArray(records) || records.length === 0) {
|
|
736
|
+
throw new ValidationError("records must be a non-empty array");
|
|
737
|
+
}
|
|
738
|
+
if (options.verifyOnChain) {
|
|
739
|
+
requireSigner(options.signer);
|
|
740
|
+
}
|
|
741
|
+
const batchSize = clampInt(options.batchSize, 1, MAX_BATCH_SIZE, DEFAULT_BATCH_SIZE);
|
|
742
|
+
const onConflict = options.onConflict ?? "update";
|
|
743
|
+
const result = { total: records.length, created: 0, updated: 0, skipped: 0, errors: [] };
|
|
744
|
+
const verifyQueue = [];
|
|
745
|
+
const verifyIds = [];
|
|
746
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
747
|
+
const batch = records.slice(i, i + batchSize);
|
|
748
|
+
const batchResults = await Promise.allSettled(
|
|
749
|
+
batch.map((record) => this.processRecord(record, onConflict))
|
|
750
|
+
);
|
|
751
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
752
|
+
const settled = batchResults[j];
|
|
753
|
+
const record = batch[j];
|
|
754
|
+
if (settled.status === "fulfilled") {
|
|
755
|
+
const outcome = settled.value;
|
|
756
|
+
if (outcome.action === "created") result.created++;
|
|
757
|
+
else if (outcome.action === "updated") result.updated++;
|
|
758
|
+
else result.skipped++;
|
|
759
|
+
if (options.verifyOnChain && record.walletAddress && record.country && outcome.action !== "skipped") {
|
|
760
|
+
verifyQueue.push({
|
|
761
|
+
walletAddress: record.walletAddress,
|
|
762
|
+
country: record.country,
|
|
763
|
+
investorType: record.investorType,
|
|
764
|
+
kycHash: record.kycHash,
|
|
765
|
+
softExpiryDays: options.verificationDefaults?.softExpiryDays,
|
|
766
|
+
hardExpiryDays: options.verificationDefaults?.hardExpiryDays
|
|
767
|
+
});
|
|
768
|
+
verifyIds.push(record.externalId);
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
const err = settled.reason;
|
|
772
|
+
result.errors.push({
|
|
773
|
+
externalId: record.externalId,
|
|
774
|
+
code: err?.code ?? "UNKNOWN",
|
|
775
|
+
message: err?.message ?? "Unknown error"
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
options.onProgress?.(Math.min(i + batchSize, records.length), records.length);
|
|
780
|
+
}
|
|
781
|
+
if (verifyQueue.length > 0) {
|
|
782
|
+
await this.runBatchVerification(verifyQueue, verifyIds, options, result);
|
|
783
|
+
}
|
|
784
|
+
return result;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Export all customers from Trusset as flat records.
|
|
788
|
+
* Useful for syncing data back into your system.
|
|
789
|
+
*/
|
|
790
|
+
async exportRecords(options = {}) {
|
|
791
|
+
const allRecords = [];
|
|
792
|
+
let offset = 0;
|
|
793
|
+
const limit = 200;
|
|
794
|
+
while (true) {
|
|
795
|
+
const { data, metadata } = await this.http.getWithMeta("/customers/api/manage", {
|
|
796
|
+
limit,
|
|
797
|
+
offset,
|
|
798
|
+
includeArchived: options.includeArchived ? "true" : void 0
|
|
799
|
+
});
|
|
800
|
+
const customers = Array.isArray(data) ? data : [];
|
|
801
|
+
if (customers.length === 0) break;
|
|
802
|
+
for (const c of customers) {
|
|
803
|
+
allRecords.push({
|
|
804
|
+
externalId: c.externalId ?? c.id,
|
|
805
|
+
walletAddress: c.walletAddress ?? void 0,
|
|
806
|
+
name: c.name ?? void 0,
|
|
807
|
+
country: c.country ?? void 0,
|
|
808
|
+
investorType: c.investorType ?? void 0,
|
|
809
|
+
kycHash: c.kycHash ?? void 0,
|
|
810
|
+
metadata: c.metadata ?? void 0
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
offset += customers.length;
|
|
814
|
+
if (metadata.total !== void 0 && offset >= metadata.total) break;
|
|
815
|
+
}
|
|
816
|
+
return allRecords;
|
|
817
|
+
}
|
|
818
|
+
// -----------------------------------------------------------------------
|
|
819
|
+
// Internal
|
|
820
|
+
// -----------------------------------------------------------------------
|
|
821
|
+
async processRecord(record, onConflict) {
|
|
822
|
+
requireString(record.externalId, "externalId");
|
|
823
|
+
let existing = null;
|
|
824
|
+
try {
|
|
825
|
+
existing = await this.manage.searchByExternalId(record.externalId);
|
|
826
|
+
} catch (err) {
|
|
827
|
+
const code = err?.code;
|
|
828
|
+
const statusCode = err?.statusCode;
|
|
829
|
+
if (statusCode !== 404 && code !== "NOT_FOUND") throw err;
|
|
830
|
+
}
|
|
831
|
+
if (existing) {
|
|
832
|
+
if (onConflict === "skip") {
|
|
833
|
+
return { action: "skipped" };
|
|
834
|
+
}
|
|
835
|
+
await this.manage.update(existing.id, {
|
|
836
|
+
name: record.name,
|
|
837
|
+
country: record.country,
|
|
838
|
+
investorType: record.investorType,
|
|
839
|
+
walletAddress: record.walletAddress,
|
|
840
|
+
metadata: record.metadata
|
|
841
|
+
});
|
|
842
|
+
return { action: "updated" };
|
|
843
|
+
}
|
|
844
|
+
await this.manage.create({
|
|
845
|
+
externalId: record.externalId,
|
|
846
|
+
walletAddress: record.walletAddress,
|
|
847
|
+
name: record.name,
|
|
848
|
+
country: record.country,
|
|
849
|
+
investorType: record.investorType,
|
|
850
|
+
metadata: record.metadata
|
|
851
|
+
});
|
|
852
|
+
return { action: "created" };
|
|
853
|
+
}
|
|
854
|
+
async runBatchVerification(queue, ids, options, result) {
|
|
855
|
+
const signer = requireSigner(options.signer);
|
|
856
|
+
for (let i = 0; i < queue.length; i += MAX_VERIFY_BATCH) {
|
|
857
|
+
const batch = queue.slice(i, i + MAX_VERIFY_BATCH);
|
|
858
|
+
try {
|
|
859
|
+
const confirmed = await this.identity.batchVerify(batch, {
|
|
860
|
+
signer,
|
|
861
|
+
confirmations: options.confirmations
|
|
862
|
+
});
|
|
863
|
+
for (const rejection of confirmed.rejected) {
|
|
864
|
+
result.errors.push({
|
|
865
|
+
externalId: ids[i + rejection.index] ?? rejection.walletAddress ?? `entry ${i + rejection.index}`,
|
|
866
|
+
code: rejection.code,
|
|
867
|
+
message: rejection.message
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
} catch (err) {
|
|
871
|
+
for (const [k, entry] of batch.entries()) {
|
|
872
|
+
result.errors.push({
|
|
873
|
+
externalId: ids[i + k] ?? entry.walletAddress,
|
|
874
|
+
code: "VERIFY_FAILED",
|
|
875
|
+
message: err?.message ?? "On-chain batch verification failed"
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// src/modules/customers/verification-profiles.ts
|
|
884
|
+
var CustomerVerificationProfiles = class {
|
|
885
|
+
constructor(http) {
|
|
886
|
+
this.http = http;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* List all verification profiles of the current instance,
|
|
890
|
+
* presets (RETAIL, PROFESSIONAL, ELIGIBLE_COUNTERPARTY) first.
|
|
891
|
+
*/
|
|
892
|
+
async list() {
|
|
893
|
+
return this.http.get("/customers/api/verification-profiles");
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// src/modules/customers/index.ts
|
|
898
|
+
var Customers = class {
|
|
899
|
+
constructor(http, chain) {
|
|
900
|
+
this.manage = new CustomerManagement(http);
|
|
901
|
+
this.identity = new CustomerIdentity(http, chain);
|
|
902
|
+
this.idLinks = new CustomerIdLinks(http);
|
|
903
|
+
this.profiles = new CustomerVerificationProfiles(http);
|
|
904
|
+
this.sync = new CustomerSync(http, this.identity);
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
export {
|
|
909
|
+
CustomerManagement,
|
|
910
|
+
manifestToVerifyInput,
|
|
911
|
+
CustomerIdentity,
|
|
912
|
+
CustomerIdLinks,
|
|
913
|
+
CustomerSync,
|
|
914
|
+
CustomerVerificationProfiles,
|
|
915
|
+
Customers
|
|
916
|
+
};
|