opacity-issuer-core 0.2.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/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/chunk-2STMWRIV.js +164 -0
- package/dist/chunk-2STMWRIV.js.map +1 -0
- package/dist/chunk-3L7NNVUV.cjs +184 -0
- package/dist/chunk-3L7NNVUV.cjs.map +1 -0
- package/dist/chunk-54Q3MU62.js +29 -0
- package/dist/chunk-54Q3MU62.js.map +1 -0
- package/dist/chunk-7ODYBEZH.js +889 -0
- package/dist/chunk-7ODYBEZH.js.map +1 -0
- package/dist/chunk-GMPQFX25.cjs +923 -0
- package/dist/chunk-GMPQFX25.cjs.map +1 -0
- package/dist/chunk-HJQZWNZJ.cjs +33 -0
- package/dist/chunk-HJQZWNZJ.cjs.map +1 -0
- package/dist/documents/index.cjs +170 -0
- package/dist/documents/index.cjs.map +1 -0
- package/dist/documents/index.d.cts +1370 -0
- package/dist/documents/index.d.ts +1370 -0
- package/dist/documents/index.js +5 -0
- package/dist/documents/index.js.map +1 -0
- package/dist/documents/validate.cjs +53 -0
- package/dist/documents/validate.cjs.map +1 -0
- package/dist/documents/validate.d.cts +70 -0
- package/dist/documents/validate.d.ts +70 -0
- package/dist/documents/validate.js +4 -0
- package/dist/documents/validate.js.map +1 -0
- package/dist/index.cjs +8917 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +11088 -0
- package/dist/index.d.ts +11088 -0
- package/dist/index.js +8647 -0
- package/dist/index.js.map +1 -0
- package/dist/subgraph.cjs +891 -0
- package/dist/subgraph.cjs.map +1 -0
- package/dist/subgraph.d.cts +456 -0
- package/dist/subgraph.d.ts +456 -0
- package/dist/subgraph.js +882 -0
- package/dist/subgraph.js.map +1 -0
- package/dist/transport-0_X4HTI6.d.ts +39 -0
- package/dist/transport-uaC5Yl3p.d.cts +39 -0
- package/dist/types-B4Yi7WA4.d.cts +108 -0
- package/dist/types-B4Yi7WA4.d.ts +108 -0
- package/package.json +75 -0
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
import { Hex, Address } from 'viem';
|
|
2
|
+
import { C as ClockOptions, b as BuildDocumentAcceptanceTypedDataParams, B as BuildAttestDocumentTransactionParams, T as TransactionRequest } from '../types-B4Yi7WA4.js';
|
|
3
|
+
export { A as AttestationErrorCode, a as AttestationSession, D as DocumentAttestorContext } from '../types-B4Yi7WA4.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal ABI for `DocumentAttestor`, hand-written rather than copied from a Foundry artifact.
|
|
7
|
+
*
|
|
8
|
+
* The contracts repo's `release-abis.yml` only syncs `AllowlistRegistry.json` downstream, so there
|
|
9
|
+
* is no artifact pipeline to consume here; a hand-written `as const` array also keeps viem's type
|
|
10
|
+
* inference sharp and the published bundle small.
|
|
11
|
+
*
|
|
12
|
+
* It covers the write path this SDK builds (`attestDocument`), the reads worth verifying a
|
|
13
|
+
* deployment against, and the errors reachable from `attestDocument` — including the two from
|
|
14
|
+
* `AllowlistRegistry` and `EnforcedPause`, which bubble up through `enableTraitAsWriter` and are
|
|
15
|
+
* the likeliest failures in practice (writer role never granted, trait not writer-enableable,
|
|
16
|
+
* registry paused). Pass this ABI to viem's revert decoding to get a named error instead of a raw
|
|
17
|
+
* selector.
|
|
18
|
+
*
|
|
19
|
+
* Owner-only functions (`setServerSigner`, the `Ownable2Step` surface) are deliberately absent —
|
|
20
|
+
* they are outside this module's scope.
|
|
21
|
+
*/
|
|
22
|
+
declare const documentAttestorAbi: readonly [{
|
|
23
|
+
readonly type: "function";
|
|
24
|
+
readonly name: "attestDocument";
|
|
25
|
+
readonly stateMutability: "nonpayable";
|
|
26
|
+
readonly inputs: readonly [{
|
|
27
|
+
readonly name: "wallet";
|
|
28
|
+
readonly type: "address";
|
|
29
|
+
}, {
|
|
30
|
+
readonly name: "traitId";
|
|
31
|
+
readonly type: "bytes32";
|
|
32
|
+
}, {
|
|
33
|
+
readonly name: "docHash";
|
|
34
|
+
readonly type: "bytes32";
|
|
35
|
+
}, {
|
|
36
|
+
readonly name: "nonce";
|
|
37
|
+
readonly type: "bytes32";
|
|
38
|
+
}, {
|
|
39
|
+
readonly name: "issuedAt";
|
|
40
|
+
readonly type: "uint256";
|
|
41
|
+
}, {
|
|
42
|
+
readonly name: "deadline";
|
|
43
|
+
readonly type: "uint256";
|
|
44
|
+
}, {
|
|
45
|
+
readonly name: "serverSignature";
|
|
46
|
+
readonly type: "bytes";
|
|
47
|
+
}, {
|
|
48
|
+
readonly name: "userSignature";
|
|
49
|
+
readonly type: "bytes";
|
|
50
|
+
}];
|
|
51
|
+
readonly outputs: readonly [];
|
|
52
|
+
}, {
|
|
53
|
+
readonly type: "function";
|
|
54
|
+
readonly name: "serverSigner";
|
|
55
|
+
readonly stateMutability: "view";
|
|
56
|
+
readonly inputs: readonly [];
|
|
57
|
+
readonly outputs: readonly [{
|
|
58
|
+
readonly type: "address";
|
|
59
|
+
}];
|
|
60
|
+
}, {
|
|
61
|
+
readonly type: "function";
|
|
62
|
+
readonly name: "usedNonces";
|
|
63
|
+
readonly stateMutability: "view";
|
|
64
|
+
readonly inputs: readonly [{
|
|
65
|
+
readonly name: "nonce";
|
|
66
|
+
readonly type: "bytes32";
|
|
67
|
+
}];
|
|
68
|
+
readonly outputs: readonly [{
|
|
69
|
+
readonly type: "bool";
|
|
70
|
+
}];
|
|
71
|
+
}, {
|
|
72
|
+
readonly type: "function";
|
|
73
|
+
readonly name: "retiredSigners";
|
|
74
|
+
readonly stateMutability: "view";
|
|
75
|
+
readonly inputs: readonly [{
|
|
76
|
+
readonly name: "signer";
|
|
77
|
+
readonly type: "address";
|
|
78
|
+
}];
|
|
79
|
+
readonly outputs: readonly [{
|
|
80
|
+
readonly type: "bool";
|
|
81
|
+
}];
|
|
82
|
+
}, {
|
|
83
|
+
readonly type: "function";
|
|
84
|
+
readonly name: "allowlistRegistry";
|
|
85
|
+
readonly stateMutability: "view";
|
|
86
|
+
readonly inputs: readonly [];
|
|
87
|
+
readonly outputs: readonly [{
|
|
88
|
+
readonly type: "address";
|
|
89
|
+
}];
|
|
90
|
+
}, {
|
|
91
|
+
readonly type: "function";
|
|
92
|
+
readonly name: "MAX_ATTESTATION_LIFETIME";
|
|
93
|
+
readonly stateMutability: "view";
|
|
94
|
+
readonly inputs: readonly [];
|
|
95
|
+
readonly outputs: readonly [{
|
|
96
|
+
readonly type: "uint256";
|
|
97
|
+
}];
|
|
98
|
+
}, {
|
|
99
|
+
readonly type: "function";
|
|
100
|
+
readonly name: "DOCUMENT_ACCEPTANCE_TYPEHASH";
|
|
101
|
+
readonly stateMutability: "view";
|
|
102
|
+
readonly inputs: readonly [];
|
|
103
|
+
readonly outputs: readonly [{
|
|
104
|
+
readonly type: "bytes32";
|
|
105
|
+
}];
|
|
106
|
+
}, {
|
|
107
|
+
readonly type: "function";
|
|
108
|
+
readonly name: "ATTESTATION_AUTHORIZATION_TYPEHASH";
|
|
109
|
+
readonly stateMutability: "view";
|
|
110
|
+
readonly inputs: readonly [];
|
|
111
|
+
readonly outputs: readonly [{
|
|
112
|
+
readonly type: "bytes32";
|
|
113
|
+
}];
|
|
114
|
+
}, {
|
|
115
|
+
readonly type: "function";
|
|
116
|
+
readonly name: "eip712Domain";
|
|
117
|
+
readonly stateMutability: "view";
|
|
118
|
+
readonly inputs: readonly [];
|
|
119
|
+
readonly outputs: readonly [{
|
|
120
|
+
readonly name: "fields";
|
|
121
|
+
readonly type: "bytes1";
|
|
122
|
+
}, {
|
|
123
|
+
readonly name: "name";
|
|
124
|
+
readonly type: "string";
|
|
125
|
+
}, {
|
|
126
|
+
readonly name: "version";
|
|
127
|
+
readonly type: "string";
|
|
128
|
+
}, {
|
|
129
|
+
readonly name: "chainId";
|
|
130
|
+
readonly type: "uint256";
|
|
131
|
+
}, {
|
|
132
|
+
readonly name: "verifyingContract";
|
|
133
|
+
readonly type: "address";
|
|
134
|
+
}, {
|
|
135
|
+
readonly name: "salt";
|
|
136
|
+
readonly type: "bytes32";
|
|
137
|
+
}, {
|
|
138
|
+
readonly name: "extensions";
|
|
139
|
+
readonly type: "uint256[]";
|
|
140
|
+
}];
|
|
141
|
+
}, {
|
|
142
|
+
readonly type: "event";
|
|
143
|
+
readonly name: "DocumentAttested";
|
|
144
|
+
readonly inputs: readonly [{
|
|
145
|
+
readonly name: "userId";
|
|
146
|
+
readonly type: "bytes32";
|
|
147
|
+
readonly indexed: true;
|
|
148
|
+
}, {
|
|
149
|
+
readonly name: "wallet";
|
|
150
|
+
readonly type: "address";
|
|
151
|
+
readonly indexed: true;
|
|
152
|
+
}, {
|
|
153
|
+
readonly name: "traitId";
|
|
154
|
+
readonly type: "bytes32";
|
|
155
|
+
readonly indexed: true;
|
|
156
|
+
}, {
|
|
157
|
+
readonly name: "docHash";
|
|
158
|
+
readonly type: "bytes32";
|
|
159
|
+
readonly indexed: false;
|
|
160
|
+
}, {
|
|
161
|
+
readonly name: "nonce";
|
|
162
|
+
readonly type: "bytes32";
|
|
163
|
+
readonly indexed: false;
|
|
164
|
+
}];
|
|
165
|
+
readonly anonymous: false;
|
|
166
|
+
}, {
|
|
167
|
+
readonly type: "error";
|
|
168
|
+
readonly name: "ZeroAddress";
|
|
169
|
+
readonly inputs: readonly [];
|
|
170
|
+
}, {
|
|
171
|
+
readonly type: "error";
|
|
172
|
+
readonly name: "InvalidTraitId";
|
|
173
|
+
readonly inputs: readonly [];
|
|
174
|
+
}, {
|
|
175
|
+
readonly type: "error";
|
|
176
|
+
readonly name: "InvalidDocHash";
|
|
177
|
+
readonly inputs: readonly [];
|
|
178
|
+
}, {
|
|
179
|
+
readonly type: "error";
|
|
180
|
+
readonly name: "NonceAlreadyUsed";
|
|
181
|
+
readonly inputs: readonly [];
|
|
182
|
+
}, {
|
|
183
|
+
readonly type: "error";
|
|
184
|
+
readonly name: "AttestationExpired";
|
|
185
|
+
readonly inputs: readonly [{
|
|
186
|
+
readonly name: "deadline";
|
|
187
|
+
readonly type: "uint256";
|
|
188
|
+
}];
|
|
189
|
+
}, {
|
|
190
|
+
readonly type: "error";
|
|
191
|
+
readonly name: "AttestationDeadlineTooFar";
|
|
192
|
+
readonly inputs: readonly [{
|
|
193
|
+
readonly name: "deadline";
|
|
194
|
+
readonly type: "uint256";
|
|
195
|
+
}, {
|
|
196
|
+
readonly name: "maxAllowed";
|
|
197
|
+
readonly type: "uint256";
|
|
198
|
+
}];
|
|
199
|
+
}, {
|
|
200
|
+
readonly type: "error";
|
|
201
|
+
readonly name: "AttestationIssuedInFuture";
|
|
202
|
+
readonly inputs: readonly [{
|
|
203
|
+
readonly name: "issuedAt";
|
|
204
|
+
readonly type: "uint256";
|
|
205
|
+
}];
|
|
206
|
+
}, {
|
|
207
|
+
readonly type: "error";
|
|
208
|
+
readonly name: "WalletNotOnboarded";
|
|
209
|
+
readonly inputs: readonly [{
|
|
210
|
+
readonly name: "wallet";
|
|
211
|
+
readonly type: "address";
|
|
212
|
+
}];
|
|
213
|
+
}, {
|
|
214
|
+
readonly type: "error";
|
|
215
|
+
readonly name: "InvalidServerSignature";
|
|
216
|
+
readonly inputs: readonly [];
|
|
217
|
+
}, {
|
|
218
|
+
readonly type: "error";
|
|
219
|
+
readonly name: "SignerRetired";
|
|
220
|
+
readonly inputs: readonly [{
|
|
221
|
+
readonly name: "signer";
|
|
222
|
+
readonly type: "address";
|
|
223
|
+
}];
|
|
224
|
+
}, {
|
|
225
|
+
readonly type: "error";
|
|
226
|
+
readonly name: "InvalidUserSignature";
|
|
227
|
+
readonly inputs: readonly [];
|
|
228
|
+
}, {
|
|
229
|
+
readonly type: "error";
|
|
230
|
+
readonly name: "ECDSAInvalidSignature";
|
|
231
|
+
readonly inputs: readonly [];
|
|
232
|
+
}, {
|
|
233
|
+
readonly type: "error";
|
|
234
|
+
readonly name: "ECDSAInvalidSignatureLength";
|
|
235
|
+
readonly inputs: readonly [{
|
|
236
|
+
readonly name: "length";
|
|
237
|
+
readonly type: "uint256";
|
|
238
|
+
}];
|
|
239
|
+
}, {
|
|
240
|
+
readonly type: "error";
|
|
241
|
+
readonly name: "ECDSAInvalidSignatureS";
|
|
242
|
+
readonly inputs: readonly [{
|
|
243
|
+
readonly name: "s";
|
|
244
|
+
readonly type: "bytes32";
|
|
245
|
+
}];
|
|
246
|
+
}, {
|
|
247
|
+
readonly type: "error";
|
|
248
|
+
readonly name: "NotTraitWriter";
|
|
249
|
+
readonly inputs: readonly [];
|
|
250
|
+
}, {
|
|
251
|
+
readonly type: "error";
|
|
252
|
+
readonly name: "TraitNotWriterEnableable";
|
|
253
|
+
readonly inputs: readonly [];
|
|
254
|
+
}, {
|
|
255
|
+
readonly type: "error";
|
|
256
|
+
readonly name: "EnforcedPause";
|
|
257
|
+
readonly inputs: readonly [];
|
|
258
|
+
}];
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* EIP-712 domain `name` and `version`, from `EIP712("DocumentAttestor", "1")` in
|
|
262
|
+
* `DocumentAttestor.sol`. The domain has no `salt` — OpenZeppelin's `EIP712` uses
|
|
263
|
+
* `EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)`.
|
|
264
|
+
*/
|
|
265
|
+
declare const DOCUMENT_ATTESTOR_DOMAIN_NAME = "DocumentAttestor";
|
|
266
|
+
declare const DOCUMENT_ATTESTOR_DOMAIN_VERSION = "1";
|
|
267
|
+
/**
|
|
268
|
+
* The payload the investor's wallet signs.
|
|
269
|
+
*
|
|
270
|
+
* It deliberately binds neither `wallet` nor `traitId`: the trait is an internal mapping and the
|
|
271
|
+
* prompt should show only what the human is agreeing to, while the binding to a wallet comes from
|
|
272
|
+
* ECDSA recovery (or, for ERC-1271, from the server's own signature). Cross-deployment and
|
|
273
|
+
* cross-chain replay are stopped by the domain's `chainId` + `verifyingContract`.
|
|
274
|
+
*
|
|
275
|
+
* Field order is consensus-critical — it is part of the type hash.
|
|
276
|
+
*/
|
|
277
|
+
declare const DOCUMENT_ACCEPTANCE_TYPES: {
|
|
278
|
+
readonly DocumentAcceptance: readonly [{
|
|
279
|
+
readonly name: "nonce";
|
|
280
|
+
readonly type: "bytes32";
|
|
281
|
+
}, {
|
|
282
|
+
readonly name: "docHash";
|
|
283
|
+
readonly type: "bytes32";
|
|
284
|
+
}, {
|
|
285
|
+
readonly name: "deadline";
|
|
286
|
+
readonly type: "uint256";
|
|
287
|
+
}];
|
|
288
|
+
};
|
|
289
|
+
declare const DOCUMENT_ACCEPTANCE_PRIMARY_TYPE = "DocumentAcceptance";
|
|
290
|
+
/**
|
|
291
|
+
* The EIP-712 domain type, needed only by `toEip712JsonPayload`: raw `eth_signTypedData_v4`
|
|
292
|
+
* requires `EIP712Domain` to be present in `types`, whereas viem derives it and ethers v6 throws
|
|
293
|
+
* if you supply it.
|
|
294
|
+
*/
|
|
295
|
+
declare const EIP712_DOMAIN_TYPE: readonly [{
|
|
296
|
+
readonly name: "name";
|
|
297
|
+
readonly type: "string";
|
|
298
|
+
}, {
|
|
299
|
+
readonly name: "version";
|
|
300
|
+
readonly type: "string";
|
|
301
|
+
}, {
|
|
302
|
+
readonly name: "chainId";
|
|
303
|
+
readonly type: "uint256";
|
|
304
|
+
}, {
|
|
305
|
+
readonly name: "verifyingContract";
|
|
306
|
+
readonly type: "address";
|
|
307
|
+
}];
|
|
308
|
+
/**
|
|
309
|
+
* `keccak256("DocumentAcceptance(bytes32 nonce,bytes32 docHash,uint256 deadline)")`.
|
|
310
|
+
*
|
|
311
|
+
* Not used to build the payload (viem derives it from {@link DOCUMENT_ACCEPTANCE_TYPES}); exported
|
|
312
|
+
* so a consumer can assert it against the contract's `DOCUMENT_ACCEPTANCE_TYPEHASH()` getter.
|
|
313
|
+
*/
|
|
314
|
+
declare const DOCUMENT_ACCEPTANCE_TYPEHASH: Hex;
|
|
315
|
+
/**
|
|
316
|
+
* Mirrors `DocumentAttestor.MAX_ATTESTATION_LIFETIME` (30 days).
|
|
317
|
+
*
|
|
318
|
+
* Duplicated off-chain so `deadline > issuedAt + MAX_ATTESTATION_LIFETIME` can be caught before the
|
|
319
|
+
* investor is prompted, rather than at redemption. It is a `constant` on a non-upgradeable contract,
|
|
320
|
+
* so it cannot drift for a given deployment — but a successor deployment could choose a different
|
|
321
|
+
* value, which is why `MAX_ATTESTATION_LIFETIME` is kept in `documentAttestorAbi` for
|
|
322
|
+
* consumers that want to read it back.
|
|
323
|
+
*/
|
|
324
|
+
declare const MAX_ATTESTATION_LIFETIME_SECONDS = 2592000n;
|
|
325
|
+
/** Length in bytes of the ECDSA signature `ECDSA.recover` accepts — it rejects every other length. */
|
|
326
|
+
declare const ECDSA_SIGNATURE_BYTES = 65;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Derives a trait id from its canonical name: `keccak256(utf8(name))`.
|
|
330
|
+
*
|
|
331
|
+
* Matches the convention the contracts use verbatim — `keccak256("doc.offering7.subscription.v1")`
|
|
332
|
+
* in Solidity is `documentTraitId('doc.offering7.subscription.v1')` here.
|
|
333
|
+
*
|
|
334
|
+
* The document VERSION is part of the name, deliberately: amendments roll the id forward, so a
|
|
335
|
+
* policy that requires `…v2` makes every `…v1` attestation inert without touching a contract.
|
|
336
|
+
*
|
|
337
|
+
* The resulting id must be writer-enableable on the registry
|
|
338
|
+
* (`AllowlistRegistry.setWriterEnableableTrait`) or `attestDocument` reverts
|
|
339
|
+
* `TraitNotWriterEnableable`.
|
|
340
|
+
*/
|
|
341
|
+
declare function documentTraitId(name: string): Hex;
|
|
342
|
+
/**
|
|
343
|
+
* Hashes a document to the `docHash` an attestation binds to.
|
|
344
|
+
*
|
|
345
|
+
* Must be the EXECUTED document — the post-signature artifact — not the blank template. Pass the
|
|
346
|
+
* raw bytes (e.g. a `Uint8Array` of the PDF); a `string` is hashed as UTF-8.
|
|
347
|
+
*/
|
|
348
|
+
declare function hashDocument(content: Uint8Array | string): Hex;
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Asserts that the bytes the app rendered still hash to the `docHash` the investor is about to
|
|
352
|
+
* sign.
|
|
353
|
+
*
|
|
354
|
+
* **This is the single most security-relevant check the client half performs**, and the only thing
|
|
355
|
+
* tying a signature to real paperwork. The 2-of-2 design defends against a compromised server
|
|
356
|
+
* acting alone and against an investor acting alone; it does nothing about a client that signs
|
|
357
|
+
* whatever `docHash` arrives over the wire, because the attestor never sees the document at all —
|
|
358
|
+
* only its hash.
|
|
359
|
+
*
|
|
360
|
+
* Since the client is what produced and submitted that hash in the first place, a mismatch never
|
|
361
|
+
* means "the server sent the wrong document" — the server has no document to send. It means one of:
|
|
362
|
+
*
|
|
363
|
+
* - the session echoed back a different `docHash` than the one posted to `POST /attestations`;
|
|
364
|
+
* - the bytes changed between being hashed and being rendered;
|
|
365
|
+
* - the wrong artifact is in hand — a blank template, or a re-encoded copy of the executed one.
|
|
366
|
+
*
|
|
367
|
+
* Call it before `buildDocumentAcceptanceTypedData` and refuse to raise the wallet prompt if it
|
|
368
|
+
* throws. Pass the EXECUTED artifact — the same post-signature bytes that were hashed for
|
|
369
|
+
* `POST /attestations` — not the blank template, and not a re-rendered or re-encoded copy of it.
|
|
370
|
+
* Hashing is byte-exact: a PDF that has been through a viewer's save path will not match even
|
|
371
|
+
* though it looks identical.
|
|
372
|
+
*
|
|
373
|
+
* The comparison is case-insensitive. `hashDocument` returns lower-case hex, but a `docHash` that
|
|
374
|
+
* arrived inside a JSON session payload may be upper- or mixed-case, and rejecting a valid document
|
|
375
|
+
* over hex casing would be a silent, extremely confusing failure. Constant-time comparison is not
|
|
376
|
+
* warranted: both values are public, and anyone in a position to substitute the hash already knows
|
|
377
|
+
* the one they substituted.
|
|
378
|
+
*
|
|
379
|
+
* @throws {OpacityAttestationError} `DocHashMismatch` when the content hashes to something other
|
|
380
|
+
* than `expectedDocHash`; `InvalidDocHash` / `MalformedInput` when `expectedDocHash` is zero or
|
|
381
|
+
* not 32 bytes of hex, or when `content` is empty or of the wrong type.
|
|
382
|
+
*/
|
|
383
|
+
declare function assertDocumentMatchesHash(content: Uint8Array | string, expectedDocHash: Hex): void;
|
|
384
|
+
/**
|
|
385
|
+
* The parts of a `POST /attestations` response this module reads.
|
|
386
|
+
*
|
|
387
|
+
* Declared structurally, so the full response type — or a raw `JSON.parse` result — satisfies it
|
|
388
|
+
* without conversion, and so this module does not have to move every time the rest of the response
|
|
389
|
+
* grows a field.
|
|
390
|
+
*/
|
|
391
|
+
interface AttestationSessionResponseLike {
|
|
392
|
+
/**
|
|
393
|
+
* Top-level echo of the hash the client submitted. Optional here only for tolerance; the locked
|
|
394
|
+
* schema always sends it, and it is cross-checked against the signed copy when present.
|
|
395
|
+
*/
|
|
396
|
+
docHash?: Hex | undefined;
|
|
397
|
+
/** Session expiry, unix seconds. A NUMBER on the locked wire; a bigint or decimal string is fine. */
|
|
398
|
+
deadline: number | bigint | string;
|
|
399
|
+
/** The EIP-712 payload the wallet is about to sign. Only `message` is read. */
|
|
400
|
+
typedData: {
|
|
401
|
+
message: {
|
|
402
|
+
/** The copy of the hash that is actually signed — this is the one that gets checked. */
|
|
403
|
+
docHash: Hex;
|
|
404
|
+
/** Decimal STRING on the wire, since `eth_signTypedData_v4` wants `uint256` as a string. */
|
|
405
|
+
deadline?: number | bigint | string | undefined;
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/** Shared by both shapes {@link verifyAttestationSession} accepts. */
|
|
410
|
+
interface VerifyAttestationSessionBase extends ClockOptions {
|
|
411
|
+
/** The document bytes the app rendered to the investor. A `string` is hashed as UTF-8. */
|
|
412
|
+
content: Uint8Array | string;
|
|
413
|
+
/**
|
|
414
|
+
* Optional, and normally absent: the client-facing session deliberately does not carry
|
|
415
|
+
* `issuedAt` — the investor does not sign it. Supply it if you have it server-side and the
|
|
416
|
+
* 30-day lifetime cap is checked too.
|
|
417
|
+
*/
|
|
418
|
+
issuedAt?: bigint | number | string;
|
|
419
|
+
}
|
|
420
|
+
/** Pass the session's fields directly, e.g. when they were carried through your own types. */
|
|
421
|
+
interface VerifyAttestationSessionFields extends VerifyAttestationSessionBase {
|
|
422
|
+
/** `docHash` exactly as the session delivered it. Compared case-insensitively. */
|
|
423
|
+
docHash: Hex;
|
|
424
|
+
/** Session expiry, unix seconds, in any of the forms the wire uses. */
|
|
425
|
+
deadline: bigint | number | string;
|
|
426
|
+
}
|
|
427
|
+
/** Hand over the `POST /attestations` response itself and let the SDK read it. */
|
|
428
|
+
interface VerifyAttestationSessionResponseParams extends VerifyAttestationSessionBase {
|
|
429
|
+
/** The session response as received. `docHash` is read from `typedData.message`. */
|
|
430
|
+
session: AttestationSessionResponseLike;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* What {@link verifyAttestationSession} needs: the rendered bytes, plus either the session response
|
|
434
|
+
* itself or the two fields read out of it.
|
|
435
|
+
*/
|
|
436
|
+
type VerifyAttestationSessionParams = VerifyAttestationSessionFields | VerifyAttestationSessionResponseParams;
|
|
437
|
+
/**
|
|
438
|
+
* The one pre-flight call a UI should make before prompting the wallet.
|
|
439
|
+
*
|
|
440
|
+
* Combines the two questions worth asking while the investor is still looking at the document: is
|
|
441
|
+
* this still the document the session commits to ({@link assertDocumentMatchesHash}), and is the
|
|
442
|
+
* session still signable (`assertSessionWindow`, so the lifetime and expiry rules live in exactly
|
|
443
|
+
* one place)?
|
|
444
|
+
*
|
|
445
|
+
* Hand it the session response as it arrived, and the fields are read from the copies that are
|
|
446
|
+
* actually signed:
|
|
447
|
+
*
|
|
448
|
+
* ```ts
|
|
449
|
+
* const session = await client.http.post('/attestations', { wallet, docHash }); // 201 body
|
|
450
|
+
*
|
|
451
|
+
* verifyAttestationSession({
|
|
452
|
+
* content: pdfBytes, // the same executed bytes that were hashed into `docHash` above
|
|
453
|
+
* session,
|
|
454
|
+
* now: BigInt(Math.floor(Date.now() / 1000)), // optional; also checks expiry
|
|
455
|
+
* });
|
|
456
|
+
*
|
|
457
|
+
* const signature = await walletClient.signTypedData({
|
|
458
|
+
* account,
|
|
459
|
+
* ...toViemTypedData(session.typedData, { expectedAttestor }),
|
|
460
|
+
* });
|
|
461
|
+
* ```
|
|
462
|
+
*
|
|
463
|
+
* viem would take `session.typedData` unchanged, but `toViemTypedData` is the shape to reach for:
|
|
464
|
+
* it validates `primaryType`, the `DocumentAcceptance` field list and its ORDER, and the domain,
|
|
465
|
+
* and `expectedAttestor` / `expectedChainId` pin the prompt to the deployment this integration
|
|
466
|
+
* trusts. This function checks the document; that one checks the payload built around it.
|
|
467
|
+
*
|
|
468
|
+
* The fields may also be passed directly — `{ content, docHash, deadline }` — when they have been
|
|
469
|
+
* carried through an app's own types. Either way `docHash` comes from `typedData.message` in
|
|
470
|
+
* preference to the top-level echo, since that is the copy the wallet signs, and `deadline` is
|
|
471
|
+
* coerced from the top-level number (a decimal string or a `bigint` works too).
|
|
472
|
+
*
|
|
473
|
+
* The document check runs first on purpose. When a session is both expired and mismatched, the
|
|
474
|
+
* mismatch is the finding that matters: an expiry is a benign retry, whereas a hash that no longer
|
|
475
|
+
* matches the bytes on screen means the backend is not committing to the document being signed,
|
|
476
|
+
* and no amount of re-issuing sessions will fix that.
|
|
477
|
+
*
|
|
478
|
+
* ### Safes, ERC-1271, and the session TTL
|
|
479
|
+
*
|
|
480
|
+
* This is where an expired session surfaces, so it is also where too short a TTL shows up. Sessions
|
|
481
|
+
* default to a 3600-second TTL (env-tunable up to the contract's 30-day `MAX_ATTESTATION_LIFETIME`
|
|
482
|
+
* cap). An ERC-1271 signer — a Safe — produces no signature until enough owners have confirmed, and
|
|
483
|
+
* that ceremony routinely outlasts an hour. The `deadline` must outlive it: issuers whose investors
|
|
484
|
+
* sign from Safes should have the TTL raised, or the session lapses mid-ceremony and the investor
|
|
485
|
+
* is asked to start over. An `AttestationExpired` from this function on a Safe flow is a TTL
|
|
486
|
+
* configuration finding, not an investor mistake.
|
|
487
|
+
*
|
|
488
|
+
* What this does *not* do: verify the server's half of the authorization. That signature is never
|
|
489
|
+
* given to the client — checking it is the attestor's job, on-chain.
|
|
490
|
+
*
|
|
491
|
+
* The clock checks stay opt-in — omit `now` and a skewed device clock cannot refuse a session the
|
|
492
|
+
* chain would have accepted. Pass `BigInt(Math.floor(Date.now() / 1000))` to enable them.
|
|
493
|
+
*
|
|
494
|
+
* @throws {OpacityAttestationError} See {@link assertDocumentMatchesHash}, plus
|
|
495
|
+
* `AttestationExpired`, `AttestationDeadlineTooFar`, `AttestationIssuedInFuture`, and
|
|
496
|
+
* `MalformedInput` for a session whose `deadline` copies disagree or cannot be read.
|
|
497
|
+
*/
|
|
498
|
+
declare function verifyAttestationSession(params: VerifyAttestationSessionParams): void;
|
|
499
|
+
|
|
500
|
+
/** The EIP-712 domain of a `DocumentAttestor` deployment. No `salt` — see `constants.ts`. */
|
|
501
|
+
interface DocumentAttestorDomain {
|
|
502
|
+
name: typeof DOCUMENT_ATTESTOR_DOMAIN_NAME;
|
|
503
|
+
version: typeof DOCUMENT_ATTESTOR_DOMAIN_VERSION;
|
|
504
|
+
chainId: number;
|
|
505
|
+
verifyingContract: Address;
|
|
506
|
+
}
|
|
507
|
+
/** What the investor's wallet is asked to sign. */
|
|
508
|
+
interface DocumentAcceptanceMessage {
|
|
509
|
+
nonce: Hex;
|
|
510
|
+
docHash: Hex;
|
|
511
|
+
deadline: bigint;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* A viem/wagmi-native EIP-712 payload.
|
|
515
|
+
*
|
|
516
|
+
* `types` deliberately omits `EIP712Domain`: viem derives the domain type from the `domain` object
|
|
517
|
+
* when the entry is absent, and ethers v6 throws if it is present. Omitting it is only safe
|
|
518
|
+
* *because* those two derive it — a raw `eth_signTypedData_v4` provider does not, so use
|
|
519
|
+
* {@link toEip712JsonPayload} / {@link toEip712Json} for that path. Never hand this shape to
|
|
520
|
+
* `eth_signTypedData_v4` directly; see {@link toEip712JsonPayload} for what silently goes wrong.
|
|
521
|
+
*/
|
|
522
|
+
interface DocumentAcceptanceTypedData {
|
|
523
|
+
domain: DocumentAttestorDomain;
|
|
524
|
+
types: typeof DOCUMENT_ACCEPTANCE_TYPES;
|
|
525
|
+
primaryType: typeof DOCUMENT_ACCEPTANCE_PRIMARY_TYPE;
|
|
526
|
+
message: DocumentAcceptanceMessage;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Builds the EIP-712 payload the investor's wallet signs to accept a document.
|
|
530
|
+
*
|
|
531
|
+
* This is a signature prompt, not a transaction: it costs no gas and broadcasts nothing. It is
|
|
532
|
+
* also the investor's *only* wallet interaction — the resulting signature is posted back to the
|
|
533
|
+
* backend, which pairs it with the server's half and relays `buildAttestDocumentTransaction` from
|
|
534
|
+
* a gas-sponsor key. The investor never sends a transaction and never needs to hold gas.
|
|
535
|
+
*
|
|
536
|
+
* The returned object is what viem and wagmi want as-is:
|
|
537
|
+
*
|
|
538
|
+
* ```ts
|
|
539
|
+
* const typedData = buildDocumentAcceptanceTypedData({
|
|
540
|
+
* attestorAddress, chainId, nonce, docHash, deadline, issuedAt,
|
|
541
|
+
* });
|
|
542
|
+
*
|
|
543
|
+
* const signature = await walletClient.signTypedData({ account, ...typedData }); // viem
|
|
544
|
+
* const signature = await signTypedDataAsync(typedData); // wagmi
|
|
545
|
+
* ```
|
|
546
|
+
*
|
|
547
|
+
* For other stacks, pass it through {@link toEthersTypedData} or {@link toEip712JsonPayload}.
|
|
548
|
+
*
|
|
549
|
+
* @throws {import('../errors.js').OpacityAttestationError} when a parameter would produce a
|
|
550
|
+
* signature the attestor is guaranteed to reject.
|
|
551
|
+
*/
|
|
552
|
+
declare function buildDocumentAcceptanceTypedData(params: BuildDocumentAcceptanceTypedDataParams): DocumentAcceptanceTypedData;
|
|
553
|
+
/**
|
|
554
|
+
* The EIP-712 digest the wallet actually signs.
|
|
555
|
+
*
|
|
556
|
+
* Useful for logging or for checking a returned signature offline. Note that an ERC-1271 wallet's
|
|
557
|
+
* signature is not recoverable to an address, so digest comparison is not a substitute for the
|
|
558
|
+
* on-chain `SignatureChecker` result.
|
|
559
|
+
*/
|
|
560
|
+
declare function hashDocumentAcceptance(typedData: DocumentAcceptanceTypedData): Hex;
|
|
561
|
+
/** ethers v6 `signer.signTypedData(domain, types, value)` takes three positional arguments. */
|
|
562
|
+
interface EthersTypedData {
|
|
563
|
+
domain: DocumentAttestorDomain;
|
|
564
|
+
/**
|
|
565
|
+
* A MUTABLE copy of {@link DOCUMENT_ACCEPTANCE_TYPES}, not the readonly constant itself:
|
|
566
|
+
* ethers v6 types `signTypedData`'s second argument as `Record<string, TypedDataField[]>`
|
|
567
|
+
* (mutable arrays), so the SDK's deeply-readonly `as const` shape fails TS2345 when passed
|
|
568
|
+
* directly. Structurally typed rather than importing ethers' `TypedDataField` — ethers is not
|
|
569
|
+
* a dependency of this package, and its types must not leak into the published declarations.
|
|
570
|
+
*/
|
|
571
|
+
types: {
|
|
572
|
+
DocumentAcceptance: {
|
|
573
|
+
name: string;
|
|
574
|
+
type: string;
|
|
575
|
+
}[];
|
|
576
|
+
};
|
|
577
|
+
value: DocumentAcceptanceMessage;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Reshapes the payload for ethers v6.
|
|
581
|
+
*
|
|
582
|
+
* ```ts
|
|
583
|
+
* const { domain, types, value } = toEthersTypedData(typedData);
|
|
584
|
+
* const signature = await signer.signTypedData(domain, types, value);
|
|
585
|
+
* ```
|
|
586
|
+
*
|
|
587
|
+
* `types` still omits `EIP712Domain` — ethers infers the domain from its first argument and throws
|
|
588
|
+
* if the type is also declared.
|
|
589
|
+
*/
|
|
590
|
+
declare function toEthersTypedData(typedData: DocumentAcceptanceTypedData): EthersTypedData;
|
|
591
|
+
/** A JSON-safe `eth_signTypedData_v4` payload: no bigints, `EIP712Domain` declared. */
|
|
592
|
+
interface Eip712JsonPayload {
|
|
593
|
+
types: Record<string, ReadonlyArray<{
|
|
594
|
+
readonly name: string;
|
|
595
|
+
readonly type: string;
|
|
596
|
+
}>>;
|
|
597
|
+
primaryType: string;
|
|
598
|
+
domain: {
|
|
599
|
+
name: string;
|
|
600
|
+
version: string;
|
|
601
|
+
chainId: number;
|
|
602
|
+
verifyingContract: Address;
|
|
603
|
+
};
|
|
604
|
+
/** `deadline` is a decimal string, not a bigint — JSON cannot carry one. */
|
|
605
|
+
message: {
|
|
606
|
+
nonce: Hex;
|
|
607
|
+
docHash: Hex;
|
|
608
|
+
deadline: string;
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Reshapes the payload for a raw EIP-1193 provider.
|
|
613
|
+
*
|
|
614
|
+
* This is the lowest common denominator every wallet connection service bottoms out at — MetaMask,
|
|
615
|
+
* WalletConnect, Coinbase Wallet, Privy, Dynamic, and the Safe Apps SDK all accept it:
|
|
616
|
+
*
|
|
617
|
+
* ```ts
|
|
618
|
+
* const signature = await provider.request({
|
|
619
|
+
* method: 'eth_signTypedData_v4',
|
|
620
|
+
* params: [account, toEip712Json(typedData)],
|
|
621
|
+
* });
|
|
622
|
+
* ```
|
|
623
|
+
*
|
|
624
|
+
* Two differences from the viem shape are required by the JSON-RPC method, not stylistic:
|
|
625
|
+
* `EIP712Domain` must be declared in `types`, and `uint256` values are decimal strings because JSON
|
|
626
|
+
* cannot carry a bigint.
|
|
627
|
+
*
|
|
628
|
+
* **Declaring `EIP712Domain` is load-bearing, and its absence fails SILENTLY.** MetaMask's
|
|
629
|
+
* `eth-sig-util` sanitises the payload with `{ EIP712Domain: [], ...types }`, so a payload that
|
|
630
|
+
* omits the entry does not error — it hashes the domain as `keccak256("EIP712Domain()")`, a type
|
|
631
|
+
* with NO fields, and the digest moves:
|
|
632
|
+
*
|
|
633
|
+
* ```text
|
|
634
|
+
* EIP712Domain declared 0x91feb0709c4ebdff9ccc57b12b26c50936eae7973f67c6d23780fcfdbda3f410 ✓
|
|
635
|
+
* EIP712Domain defaulted [] 0x82ada7044278f3b529058b2dac44bb987f078c23b15195e1dca77110f0c91691 ✗
|
|
636
|
+
* ```
|
|
637
|
+
*
|
|
638
|
+
* The wallet then returns a perfectly well-formed signature over the WRONG digest, which the
|
|
639
|
+
* backend rejects at its simulate step with `InvalidUserSignature` — far from the cause, and
|
|
640
|
+
* indistinguishable to the investor from "the wallet is broken". Declaring the type does not touch
|
|
641
|
+
* the `DocumentAcceptance` struct hash; it only makes the DOMAIN hash correct. So this is strictly
|
|
642
|
+
* a fix, never a behaviour change, and the `EIP712Domain` entry must not be removed from this
|
|
643
|
+
* function on the theory that the wallet will derive it.
|
|
644
|
+
*/
|
|
645
|
+
declare function toEip712JsonPayload(typedData: DocumentAcceptanceTypedData): Eip712JsonPayload;
|
|
646
|
+
/**
|
|
647
|
+
* {@link toEip712JsonPayload}, serialised — the second `eth_signTypedData_v4` parameter.
|
|
648
|
+
*
|
|
649
|
+
* A backend-issued payload reaches this through `toEip712Json(toViemTypedData(session.typedData))`:
|
|
650
|
+
* `toViemTypedData` validates and normalises the server's JSON, and this re-serialises it with
|
|
651
|
+
* `EIP712Domain` declared.
|
|
652
|
+
*
|
|
653
|
+
* That round trip is optional on this path. mobile-api already declares `EIP712Domain` and already
|
|
654
|
+
* sends `deadline` as a decimal string, so `session.typedData` is a correct
|
|
655
|
+
* `eth_signTypedData_v4` payload exactly as it arrives — nothing is corrupted by sending it
|
|
656
|
+
* directly. Going through `toViemTypedData` buys validation (`primaryType`, the field list and its
|
|
657
|
+
* ORDER, the domain) and `expectedAttestor` / `expectedChainId` pinning, and it re-declares
|
|
658
|
+
* `EIP712Domain` from this SDK's own constant rather than trusting the wire's copy. Prefer it, but
|
|
659
|
+
* do not treat the direct route as broken.
|
|
660
|
+
*/
|
|
661
|
+
declare function toEip712Json(typedData: DocumentAcceptanceTypedData): string;
|
|
662
|
+
|
|
663
|
+
/** `attestDocument` arguments, in the contract's positional order. */
|
|
664
|
+
type AttestDocumentArgs = readonly [
|
|
665
|
+
wallet: Address,
|
|
666
|
+
traitId: Hex,
|
|
667
|
+
docHash: Hex,
|
|
668
|
+
nonce: Hex,
|
|
669
|
+
issuedAt: bigint,
|
|
670
|
+
deadline: bigint,
|
|
671
|
+
serverSignature: Hex,
|
|
672
|
+
userSignature: Hex
|
|
673
|
+
];
|
|
674
|
+
/**
|
|
675
|
+
* Builds the `attestDocument` transaction that redeems a signed session on-chain.
|
|
676
|
+
*
|
|
677
|
+
* This is the relay half of the flow, so its consumer is the backend, not the browser. The
|
|
678
|
+
* investor's entire involvement is the off-chain signature from
|
|
679
|
+
* {@link import('./typed-data.js').buildDocumentAcceptanceTypedData} — one wallet prompt, no
|
|
680
|
+
* transaction, no gas. The relayer pairs that signature with the server's half and broadcasts this
|
|
681
|
+
* call from a gas-sponsor key.
|
|
682
|
+
*
|
|
683
|
+
* The call is permissionless — `msg.sender` is never trusted and never recorded — so `from` is
|
|
684
|
+
* optional, and when supplied it names the sponsor account paying for gas. The descriptor works
|
|
685
|
+
* as-is with viem, wagmi and ethers v6:
|
|
686
|
+
*
|
|
687
|
+
* ```ts
|
|
688
|
+
* const tx = buildAttestDocumentTransaction({ ... });
|
|
689
|
+
*
|
|
690
|
+
* await walletClient.sendTransaction({ account, ...tx }); // viem
|
|
691
|
+
* sendTransaction(tx); // wagmi
|
|
692
|
+
* await signer.sendTransaction(tx); // ethers v6
|
|
693
|
+
* ```
|
|
694
|
+
*
|
|
695
|
+
* See {@link toWriteContractRequest} for wagmi's `useWriteContract`, and
|
|
696
|
+
* {@link toJsonRpcTransaction} for a raw EIP-1193 provider.
|
|
697
|
+
*
|
|
698
|
+
* @throws {import('../errors.js').OpacityAttestationError} when a parameter would produce a
|
|
699
|
+
* transaction the attestor is guaranteed to revert.
|
|
700
|
+
*/
|
|
701
|
+
declare function buildAttestDocumentTransaction(params: BuildAttestDocumentTransactionParams): TransactionRequest;
|
|
702
|
+
/** The ABI-level form of the same call, for viem `writeContract` / wagmi `useWriteContract`. */
|
|
703
|
+
interface WriteContractRequest {
|
|
704
|
+
address: Address;
|
|
705
|
+
abi: typeof documentAttestorAbi;
|
|
706
|
+
functionName: 'attestDocument';
|
|
707
|
+
args: AttestDocumentArgs;
|
|
708
|
+
chainId: number;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* The same call expressed as contract arguments rather than calldata.
|
|
712
|
+
*
|
|
713
|
+
* Preferred by wagmi users, and it gives viem the ABI it needs to decode a revert into a named
|
|
714
|
+
* error (`NotTraitWriter`, `NonceAlreadyUsed`, …) instead of a bare selector.
|
|
715
|
+
*
|
|
716
|
+
* ```ts
|
|
717
|
+
* writeContract(toWriteContractRequest(params));
|
|
718
|
+
* ```
|
|
719
|
+
*/
|
|
720
|
+
declare function toWriteContractRequest(params: BuildAttestDocumentTransactionParams): WriteContractRequest;
|
|
721
|
+
/** An `eth_sendTransaction` parameter object — every numeric field hex-encoded. */
|
|
722
|
+
interface JsonRpcTransaction {
|
|
723
|
+
from: Address;
|
|
724
|
+
to: Address;
|
|
725
|
+
data: Hex;
|
|
726
|
+
value: Hex;
|
|
727
|
+
chainId: Hex;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Reshapes the descriptor for a raw EIP-1193 provider.
|
|
731
|
+
*
|
|
732
|
+
* `eth_sendTransaction` demands a mandatory `from` and hex-quantity encoding; the chain-agnostic
|
|
733
|
+
* descriptor has neither (its own `from` is optional and its quantities are bigints), so the
|
|
734
|
+
* sending account is passed explicitly here — under relay, that is the gas sponsor:
|
|
735
|
+
*
|
|
736
|
+
* ```ts
|
|
737
|
+
* const hash = await provider.request({
|
|
738
|
+
* method: 'eth_sendTransaction',
|
|
739
|
+
* params: [toJsonRpcTransaction(tx, { from: relayer })],
|
|
740
|
+
* });
|
|
741
|
+
* ```
|
|
742
|
+
*/
|
|
743
|
+
declare function toJsonRpcTransaction(transaction: TransactionRequest, options: {
|
|
744
|
+
from: Address;
|
|
745
|
+
}): JsonRpcTransaction;
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Minimal ABI for `SandboxAllowlistRegistry` — the SANDBOX-ONLY registry implementation.
|
|
749
|
+
*
|
|
750
|
+
* The sandbox implementation is the stock `AllowlistRegistry` plus permissionless, sandbox-only
|
|
751
|
+
* helpers (`sandboxAddSelf()`, v2's `sandboxSetSelfTrait()`, and v3's `sandboxRegisterIssuer()`).
|
|
752
|
+
* It runs behind the registry proxy in a sandbox deployment, so there is no separate helper address: this ABI is used against
|
|
753
|
+
* the `allowlistRegistry` address from discovery or the built-in `SANDBOX_DEPLOYMENTS` entry, and
|
|
754
|
+
* only works while that proxy exposes the sandbox implementation.
|
|
755
|
+
*
|
|
756
|
+
* Covers the writes this module builds (`sandboxAddSelf`, `sandboxSetSelfTrait`), the marker view
|
|
757
|
+
* that identifies the sandbox implementation behind a proxy (`sandboxVersion`), the reads a
|
|
758
|
+
* consumer verifies results with (`isAllowed`, `getUserId`, `hasGlobalTrait`), their emitted trait
|
|
759
|
+
* event, and every error reachable from those writes:
|
|
760
|
+
*
|
|
761
|
+
* - `EnforcedPause()` — the `whenNotPaused` modifier (OpenZeppelin `PausableUpgradeable`) when the
|
|
762
|
+
* registry is paused.
|
|
763
|
+
* - `AddressNotEligible(address)` — the parent `_addToAllowlist` when the sender's entry is in a
|
|
764
|
+
* TERMINAL status (`COMPLIANCE_REMOVAL` or `MIGRATED`); those cannot be re-added by any path.
|
|
765
|
+
* - `AddressNotVerified(address)` — `sandboxSetSelfTrait` requires its caller to be verified first.
|
|
766
|
+
* - `InvalidUserId()` — the caller has no usable userId binding.
|
|
767
|
+
* - `InvalidTraitId()` — `traitId` is zero.
|
|
768
|
+
*/
|
|
769
|
+
declare const sandboxAllowlistRegistryAbi: readonly [{
|
|
770
|
+
readonly type: "function";
|
|
771
|
+
readonly name: "sandboxRegisterIssuer";
|
|
772
|
+
readonly stateMutability: "nonpayable";
|
|
773
|
+
readonly inputs: readonly [];
|
|
774
|
+
readonly outputs: readonly [];
|
|
775
|
+
}, {
|
|
776
|
+
readonly type: "function";
|
|
777
|
+
readonly name: "sandboxAddSelf";
|
|
778
|
+
readonly stateMutability: "nonpayable";
|
|
779
|
+
readonly inputs: readonly [];
|
|
780
|
+
readonly outputs: readonly [];
|
|
781
|
+
}, {
|
|
782
|
+
readonly type: "function";
|
|
783
|
+
readonly name: "sandboxSetSelfTrait";
|
|
784
|
+
readonly stateMutability: "nonpayable";
|
|
785
|
+
readonly inputs: readonly [{
|
|
786
|
+
readonly name: "traitId";
|
|
787
|
+
readonly type: "bytes32";
|
|
788
|
+
}, {
|
|
789
|
+
readonly name: "enabled";
|
|
790
|
+
readonly type: "bool";
|
|
791
|
+
}];
|
|
792
|
+
readonly outputs: readonly [];
|
|
793
|
+
}, {
|
|
794
|
+
readonly type: "function";
|
|
795
|
+
readonly name: "sandboxVersion";
|
|
796
|
+
readonly stateMutability: "pure";
|
|
797
|
+
readonly inputs: readonly [];
|
|
798
|
+
readonly outputs: readonly [{
|
|
799
|
+
readonly type: "uint256";
|
|
800
|
+
}];
|
|
801
|
+
}, {
|
|
802
|
+
readonly type: "function";
|
|
803
|
+
readonly name: "isAllowed";
|
|
804
|
+
readonly stateMutability: "view";
|
|
805
|
+
readonly inputs: readonly [{
|
|
806
|
+
readonly name: "account";
|
|
807
|
+
readonly type: "address";
|
|
808
|
+
}];
|
|
809
|
+
readonly outputs: readonly [{
|
|
810
|
+
readonly type: "bool";
|
|
811
|
+
}];
|
|
812
|
+
}, {
|
|
813
|
+
readonly type: "function";
|
|
814
|
+
readonly name: "getUserId";
|
|
815
|
+
readonly stateMutability: "view";
|
|
816
|
+
readonly inputs: readonly [{
|
|
817
|
+
readonly name: "account";
|
|
818
|
+
readonly type: "address";
|
|
819
|
+
}];
|
|
820
|
+
readonly outputs: readonly [{
|
|
821
|
+
readonly type: "bytes32";
|
|
822
|
+
}];
|
|
823
|
+
}, {
|
|
824
|
+
readonly type: "function";
|
|
825
|
+
readonly name: "hasGlobalTrait";
|
|
826
|
+
readonly stateMutability: "view";
|
|
827
|
+
readonly inputs: readonly [{
|
|
828
|
+
readonly name: "userId";
|
|
829
|
+
readonly type: "bytes32";
|
|
830
|
+
}, {
|
|
831
|
+
readonly name: "traitId";
|
|
832
|
+
readonly type: "bytes32";
|
|
833
|
+
}];
|
|
834
|
+
readonly outputs: readonly [{
|
|
835
|
+
readonly type: "bool";
|
|
836
|
+
}];
|
|
837
|
+
}, {
|
|
838
|
+
readonly type: "event";
|
|
839
|
+
readonly name: "GlobalTraitSet";
|
|
840
|
+
readonly anonymous: false;
|
|
841
|
+
readonly inputs: readonly [{
|
|
842
|
+
readonly name: "userId";
|
|
843
|
+
readonly type: "bytes32";
|
|
844
|
+
readonly indexed: true;
|
|
845
|
+
}, {
|
|
846
|
+
readonly name: "traitId";
|
|
847
|
+
readonly type: "bytes32";
|
|
848
|
+
readonly indexed: true;
|
|
849
|
+
}, {
|
|
850
|
+
readonly name: "enabled";
|
|
851
|
+
readonly type: "bool";
|
|
852
|
+
readonly indexed: false;
|
|
853
|
+
}, {
|
|
854
|
+
readonly name: "updatedBy";
|
|
855
|
+
readonly type: "address";
|
|
856
|
+
readonly indexed: false;
|
|
857
|
+
}];
|
|
858
|
+
}, {
|
|
859
|
+
readonly type: "error";
|
|
860
|
+
readonly name: "EnforcedPause";
|
|
861
|
+
readonly inputs: readonly [];
|
|
862
|
+
}, {
|
|
863
|
+
readonly type: "error";
|
|
864
|
+
readonly name: "AddressNotEligible";
|
|
865
|
+
readonly inputs: readonly [{
|
|
866
|
+
readonly name: "account";
|
|
867
|
+
readonly type: "address";
|
|
868
|
+
}];
|
|
869
|
+
}, {
|
|
870
|
+
readonly type: "error";
|
|
871
|
+
readonly name: "AddressNotVerified";
|
|
872
|
+
readonly inputs: readonly [{
|
|
873
|
+
readonly name: "account";
|
|
874
|
+
readonly type: "address";
|
|
875
|
+
}];
|
|
876
|
+
}, {
|
|
877
|
+
readonly type: "error";
|
|
878
|
+
readonly name: "InvalidUserId";
|
|
879
|
+
readonly inputs: readonly [];
|
|
880
|
+
}, {
|
|
881
|
+
readonly type: "error";
|
|
882
|
+
readonly name: "InvalidTraitId";
|
|
883
|
+
readonly inputs: readonly [];
|
|
884
|
+
}];
|
|
885
|
+
/**
|
|
886
|
+
* What `sandboxVersion()` returns on the SDK's target sandbox implementation source.
|
|
887
|
+
*
|
|
888
|
+
* Mirrors the `return 3` in the deployed `SandboxAllowlistRegistry.sol`. A lower live version
|
|
889
|
+
* exposes fewer sandbox capabilities; a higher one is a newer surface this SDK may not know yet.
|
|
890
|
+
*/
|
|
891
|
+
declare const SANDBOX_IMPLEMENTATION_VERSION = 3n;
|
|
892
|
+
/**
|
|
893
|
+
* Named trait ids used by the current sandbox product flow.
|
|
894
|
+
*
|
|
895
|
+
* `sandboxSetSelfTrait` itself accepts ANY non-zero `bytes32`; these are conveniences, not an
|
|
896
|
+
* allowlist. `mmfDocumentSigned` is specifically the current MMF subscription document version.
|
|
897
|
+
* Use `documentTraitId(<versioned canonical name>)` for another document or offering.
|
|
898
|
+
*/
|
|
899
|
+
declare const SANDBOX_TRAIT_IDS: Readonly<{
|
|
900
|
+
/** `keccak256("trait.accredited")` — the contracts' canonical accredited-investor trait. */
|
|
901
|
+
readonly accreditedInvestor: `0x${string}`;
|
|
902
|
+
/** `keccak256("doc.mmf.subscription.v1")` — the current MMF subscription document trait. */
|
|
903
|
+
readonly mmfDocumentSigned: `0x${string}`;
|
|
904
|
+
}>;
|
|
905
|
+
/**
|
|
906
|
+
* Which registry proxy, on which chain, and optionally which wallet sends.
|
|
907
|
+
*
|
|
908
|
+
* Unlike the attestation flow there is no separate helper contract to point at: `registryAddress`
|
|
909
|
+
* is exactly the sandbox deployment's `allowlistRegistry` proxy returned by the discovery endpoint
|
|
910
|
+
* (`GET /chains/:chain_id/attestor`) — or, for a chain with a built-in entry,
|
|
911
|
+
* `SANDBOX_DEPLOYMENTS[chainId].allowlistRegistry`.
|
|
912
|
+
*/
|
|
913
|
+
interface BuildSandboxAddSelfTransactionParams {
|
|
914
|
+
/** The allowlist-registry PROXY address — the same one discovery returns. */
|
|
915
|
+
registryAddress: Address;
|
|
916
|
+
/** Chain the registry proxy is deployed on. */
|
|
917
|
+
chainId: number;
|
|
918
|
+
/**
|
|
919
|
+
* Optional expected sender recorded as JSON-RPC `from`. If supplied it MUST be the investor's
|
|
920
|
+
* own wallet. A viem caller must ALSO pass the same wallet as `account`; prefer the ABI-level
|
|
921
|
+
* builder when account binding is desired.
|
|
922
|
+
*/
|
|
923
|
+
from?: Address;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Builds the `sandboxAddSelf()` transaction — a SANDBOX-ONLY, permissionless KYC bypass.
|
|
927
|
+
*
|
|
928
|
+
* **SANDBOX ONLY.** `sandboxAddSelf` exists only on the `SandboxAllowlistRegistry` implementation,
|
|
929
|
+
* a deliberate, total KYC bypass for throwaway sandbox deployments. It must NEVER point at a
|
|
930
|
+
* production registry; on production implementations this calldata reverts with an unknown
|
|
931
|
+
* selector.
|
|
932
|
+
*
|
|
933
|
+
* **The SENDER is who gets allowlisted.** This is the exact opposite of `attestDocument`'s
|
|
934
|
+
* permissionless relay: the contract allowlists `msg.sender`, so this transaction CANNOT be
|
|
935
|
+
* relayed on an investor's behalf. The investor's own wallet must send it, and that wallet pays
|
|
936
|
+
* the gas.
|
|
937
|
+
*
|
|
938
|
+
* `registryAddress` is the same allowlist-registry proxy discovery returns (`allowlistRegistry`
|
|
939
|
+
* from `GET /chains/:chain_id/attestor`) — same address, same events.
|
|
940
|
+
*
|
|
941
|
+
* On success the sender holds a VERIFIED allowlist entry stamped with the registry's default
|
|
942
|
+
* expiry window, and on the FIRST call the wallet is bound to the deterministic userId
|
|
943
|
+
* {@link sandboxUserId} computes. With viem, pass the investor explicitly as `account` when
|
|
944
|
+
* sending; the raw descriptor's `from` is not an account-selection mechanism. See
|
|
945
|
+
* {@link toSandboxAddSelfWriteContractRequest} for an ABI-level request whose `account` is bound,
|
|
946
|
+
* and `toJsonRpcTransaction` from `transaction.ts` for a raw EIP-1193 provider.
|
|
947
|
+
*
|
|
948
|
+
* @throws {import('../errors.js').OpacityAttestationError} when `registryAddress`, `chainId` or
|
|
949
|
+
* `from` is malformed or zero.
|
|
950
|
+
*/
|
|
951
|
+
declare function buildSandboxAddSelfTransaction(params: BuildSandboxAddSelfTransactionParams): TransactionRequest;
|
|
952
|
+
/** The ABI-level form of the same call, for viem `writeContract` / wagmi `useWriteContract`. */
|
|
953
|
+
interface SandboxAddSelfWriteContractRequest {
|
|
954
|
+
address: Address;
|
|
955
|
+
abi: typeof sandboxAllowlistRegistryAbi;
|
|
956
|
+
functionName: 'sandboxAddSelf';
|
|
957
|
+
args: readonly [];
|
|
958
|
+
chainId: number;
|
|
959
|
+
/** Present only when the caller pinned a `from` — the investor wallet that must send. */
|
|
960
|
+
account?: Address;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* The same SANDBOX-ONLY call expressed as contract arguments rather than calldata.
|
|
964
|
+
*
|
|
965
|
+
* Preferred by wagmi users, and it gives viem the ABI it needs to decode a revert into a named
|
|
966
|
+
* error (`EnforcedPause`, `AddressNotEligible`) instead of a bare selector. All the warnings on
|
|
967
|
+
* {@link buildSandboxAddSelfTransaction} apply verbatim — sandbox only, and whichever account
|
|
968
|
+
* sends this request is the one that gets allowlisted; a supplied `from` becomes `account`.
|
|
969
|
+
*
|
|
970
|
+
* ```ts
|
|
971
|
+
* writeContract(toSandboxAddSelfWriteContractRequest({ registryAddress, chainId }));
|
|
972
|
+
* ```
|
|
973
|
+
*/
|
|
974
|
+
declare function toSandboxAddSelfWriteContractRequest(params: BuildSandboxAddSelfTransactionParams): SandboxAddSelfWriteContractRequest;
|
|
975
|
+
/** Sandbox v3 issuer registration uses the same caller/registry binding as self-onboarding. */
|
|
976
|
+
type BuildSandboxRegisterIssuerTransactionParams = BuildSandboxAddSelfTransactionParams;
|
|
977
|
+
/** Onboard and authorize the sending wallet as an issuer, without an operator transaction. */
|
|
978
|
+
declare function buildSandboxRegisterIssuerTransaction(params: BuildSandboxRegisterIssuerTransactionParams): TransactionRequest;
|
|
979
|
+
/** ABI-level registration request for the issuer's own wallet. Requires a v3 registry. */
|
|
980
|
+
type SandboxRegisterIssuerWriteContractRequest = Omit<SandboxAddSelfWriteContractRequest, 'functionName'> & {
|
|
981
|
+
functionName: 'sandboxRegisterIssuer';
|
|
982
|
+
};
|
|
983
|
+
declare function toSandboxRegisterIssuerWriteContractRequest(params: BuildSandboxRegisterIssuerTransactionParams): SandboxRegisterIssuerWriteContractRequest;
|
|
984
|
+
/** Inputs shared by the calldata and ABI-level `sandboxSetSelfTrait` builders. */
|
|
985
|
+
interface BuildSandboxSetSelfTraitTransactionParams {
|
|
986
|
+
/** The allowlist-registry PROXY running `SandboxAllowlistRegistry` v2 or newer. */
|
|
987
|
+
registryAddress: Address;
|
|
988
|
+
/** Chain the registry proxy is deployed on. */
|
|
989
|
+
chainId: number;
|
|
990
|
+
/** Any non-zero global trait id. See {@link SANDBOX_TRAIT_IDS} for current presets. */
|
|
991
|
+
traitId: Hex;
|
|
992
|
+
/** `true` grants the trait; `false` clears it. */
|
|
993
|
+
enabled: boolean;
|
|
994
|
+
/**
|
|
995
|
+
* Optional expected sender as JSON-RPC `from`; viem callers must also select it as `account`.
|
|
996
|
+
* This wallet's own userId receives (or loses) the trait.
|
|
997
|
+
*/
|
|
998
|
+
from?: Address;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Builds a `sandboxSetSelfTrait(bytes32,bool)` transaction for the caller's own global trait.
|
|
1002
|
+
*
|
|
1003
|
+
* **SANDBOX v2 OR NEWER.** This is a deliberate KYC/document/compliance bypass and is absent from the
|
|
1004
|
+
* production registry and sandbox v1. The sending wallet must already have VERIFIED status —
|
|
1005
|
+
* normally by sending {@link buildSandboxAddSelfTransaction} first — and the trait is written to
|
|
1006
|
+
* that wallet's existing userId. There is deliberately no combined add+trait transaction.
|
|
1007
|
+
*
|
|
1008
|
+
* `traitId` can be any non-zero `bytes32`. `enabled: true` grants it; `enabled: false` clears it.
|
|
1009
|
+
* Both directions emit `GlobalTraitSet` with `updatedBy` equal to the sending wallet.
|
|
1010
|
+
*
|
|
1011
|
+
* @throws {import('../errors.js').OpacityAttestationError} when an address, `chainId`, `traitId`,
|
|
1012
|
+
* or `enabled` is malformed; zero `traitId` is rejected locally as `InvalidTraitId`.
|
|
1013
|
+
*/
|
|
1014
|
+
declare function buildSandboxSetSelfTraitTransaction(params: BuildSandboxSetSelfTraitTransactionParams): TransactionRequest;
|
|
1015
|
+
/** ABI-level form of `sandboxSetSelfTrait`, for viem/wagmi `writeContract`. */
|
|
1016
|
+
interface SandboxSetSelfTraitWriteContractRequest {
|
|
1017
|
+
address: Address;
|
|
1018
|
+
abi: typeof sandboxAllowlistRegistryAbi;
|
|
1019
|
+
functionName: 'sandboxSetSelfTrait';
|
|
1020
|
+
args: readonly [traitId: Hex, enabled: boolean];
|
|
1021
|
+
chainId: number;
|
|
1022
|
+
/** Present only when the caller pins the wallet whose own trait will change. */
|
|
1023
|
+
account?: Address;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Expresses the same sandbox-v2 trait write as ABI + args for viem/wagmi.
|
|
1027
|
+
*
|
|
1028
|
+
* All warnings and validation on {@link buildSandboxSetSelfTraitTransaction} apply. A supplied
|
|
1029
|
+
* `from` becomes `account`; that account must send the transaction and is the identity modified.
|
|
1030
|
+
*/
|
|
1031
|
+
declare function toSandboxSetSelfTraitWriteContractRequest(params: BuildSandboxSetSelfTraitTransactionParams): SandboxSetSelfTraitWriteContractRequest;
|
|
1032
|
+
/**
|
|
1033
|
+
* The deterministic userId `sandboxAddSelf` binds a wallet to on its FIRST call:
|
|
1034
|
+
* `keccak256(abi.encodePacked("sandbox.user.", wallet))` — the 13 UTF-8 bytes of the prefix
|
|
1035
|
+
* concatenated with the 20 address bytes, PACKED, not ABI-padded.
|
|
1036
|
+
*
|
|
1037
|
+
* Checksum-insensitive: the address is normalised before hashing, so any casing of the same
|
|
1038
|
+
* address produces the same id.
|
|
1039
|
+
*
|
|
1040
|
+
* This predicts the userId ONLY for the first-call binding. A wallet previously onboarded through
|
|
1041
|
+
* another path (TEE batch, compliance admin, …) already has a userId, and `sandboxAddSelf` keeps
|
|
1042
|
+
* that existing binding — read the real one back with `getUserId(wallet)` rather than assuming
|
|
1043
|
+
* this value.
|
|
1044
|
+
*
|
|
1045
|
+
* @throws {import('../errors.js').OpacityAttestationError} when `wallet` is malformed or zero.
|
|
1046
|
+
*/
|
|
1047
|
+
declare function sandboxUserId(wallet: Address): Hex;
|
|
1048
|
+
/**
|
|
1049
|
+
* The RPC capabilities {@link probeSandboxRegistry} needs: bytecode lookup, chain identity, and
|
|
1050
|
+
* `eth_call`.
|
|
1051
|
+
*
|
|
1052
|
+
* A structural subset of viem's `PublicClient` — pass the public client your app already has
|
|
1053
|
+
* (wagmi's `usePublicClient()`, or `createPublicClient`). The SDK never opens an RPC connection
|
|
1054
|
+
* of its own, which is why this is a parameter and not configuration it resolves.
|
|
1055
|
+
*/
|
|
1056
|
+
interface SandboxProbeClient {
|
|
1057
|
+
/** Named `getBytecode` for compatibility with the full declared viem v2 peer range. */
|
|
1058
|
+
getBytecode(args: {
|
|
1059
|
+
address: Address;
|
|
1060
|
+
}): Promise<Hex | undefined>;
|
|
1061
|
+
getChainId(): Promise<number>;
|
|
1062
|
+
call(args: {
|
|
1063
|
+
to: Address;
|
|
1064
|
+
data: Hex;
|
|
1065
|
+
account?: Address;
|
|
1066
|
+
}): Promise<{
|
|
1067
|
+
data?: Hex | undefined;
|
|
1068
|
+
}>;
|
|
1069
|
+
}
|
|
1070
|
+
interface ProbeSandboxRegistryParams {
|
|
1071
|
+
/** The allowlist-registry PROXY to inspect. */
|
|
1072
|
+
registryAddress: Address;
|
|
1073
|
+
/** Expected RPC chain. Required so a wrong-network reader can never produce a verdict. */
|
|
1074
|
+
chainId: number;
|
|
1075
|
+
/**
|
|
1076
|
+
* Sender to impersonate in the legacy `sandboxAddSelf()` `eth_call` — the investor's wallet,
|
|
1077
|
+
* typically, so a `self_add_revert` verdict is about THEIR entry. Defaults to a nobody address.
|
|
1078
|
+
* Nothing is sent either way.
|
|
1079
|
+
*/
|
|
1080
|
+
from?: Address;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* How {@link probeSandboxRegistry} reached its verdict, most to least direct.
|
|
1084
|
+
*
|
|
1085
|
+
* - `marker` — `sandboxVersion()` answered. A sandbox implementation with the marker (source from
|
|
1086
|
+
* 2026-09-01 on) is attached; `version` carries the answer.
|
|
1087
|
+
* - `self_add_call` — the marker reverted, but an `eth_call` of `sandboxAddSelf()` from `from`
|
|
1088
|
+
* returned the empty data its no-output ABI requires. This is compatibility evidence for an
|
|
1089
|
+
* older, PRE-marker sandbox implementation, but is necessarily heuristic because a foreign
|
|
1090
|
+
* contract can have a permissive empty fallback.
|
|
1091
|
+
* - `self_add_revert` — `sandboxAddSelf()` exists but refused the probe sender with one of its own
|
|
1092
|
+
* errors (`EnforcedPause`, `AddressNotEligible`). The sandbox IS attached; the real transaction
|
|
1093
|
+
* from that sender would revert the same way right now.
|
|
1094
|
+
* - `not_sandbox` — the marker returned an invalid/empty answer, or both selectors reverted with no
|
|
1095
|
+
* sandbox registry error. A stock `AllowlistRegistry` (or something else) answers here.
|
|
1096
|
+
* - `no_code` — bytecode lookup proved nothing is deployed at the address.
|
|
1097
|
+
*/
|
|
1098
|
+
type SandboxProbeEvidence = 'marker' | 'self_add_call' | 'self_add_revert' | 'not_sandbox' | 'no_code';
|
|
1099
|
+
/** The verdict of {@link probeSandboxRegistry}. */
|
|
1100
|
+
interface SandboxRegistryProbe {
|
|
1101
|
+
/** The registry that was inspected, checksummed. */
|
|
1102
|
+
registryAddress: Address;
|
|
1103
|
+
/** Whether a `SandboxAllowlistRegistry` implementation answers behind the proxy. */
|
|
1104
|
+
attached: boolean;
|
|
1105
|
+
/**
|
|
1106
|
+
* `sandboxVersion()` when the implementation exposes it; `null` for a pre-marker sandbox
|
|
1107
|
+
* implementation and for anything that is not the sandbox. Version 2 or newer is required for
|
|
1108
|
+
* `sandboxSetSelfTrait`.
|
|
1109
|
+
*/
|
|
1110
|
+
version: bigint | null;
|
|
1111
|
+
/** How the verdict was reached. See {@link SandboxProbeEvidence}. */
|
|
1112
|
+
evidence: SandboxProbeEvidence;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Asks a registry proxy, on-chain, whether the sandbox implementation is attached — BEFORE an
|
|
1116
|
+
* investor is prompted to send `sandboxAddSelf()`.
|
|
1117
|
+
*
|
|
1118
|
+
* The sandbox registry is upgradeable, so an address alone does not prove which implementation
|
|
1119
|
+
* currently answers behind it. Two read-only `eth_call`s settle it:
|
|
1120
|
+
*
|
|
1121
|
+
* 1. `sandboxVersion()` — the marker the contracts added for exactly this purpose. Present, and
|
|
1122
|
+
* the answer is the verdict (`marker`).
|
|
1123
|
+
* 2. Otherwise `sandboxAddSelf()` itself, simulated from `from`. Older sandbox implementations
|
|
1124
|
+
* deployed before the marker existed have the function but not the view. Its expected empty
|
|
1125
|
+
* return (or a sandbox-specific revert) is a compatibility signal (`self_add_call` /
|
|
1126
|
+
* `self_add_revert`), not cryptographic proof: a permissive fallback can imitate empty success.
|
|
1127
|
+
* Marker-based version detection is authoritative for v1+.
|
|
1128
|
+
*
|
|
1129
|
+
* The reader's chain is checked first, then bytecode, because a wrong network or an empty account
|
|
1130
|
+
* must never produce a verdict. Only an execution REVERT is treated as an answer; a transport
|
|
1131
|
+
* failure propagates as thrown by the client, so a flaky node never reads as "not a sandbox".
|
|
1132
|
+
*
|
|
1133
|
+
* ```ts
|
|
1134
|
+
* const probe = await probeSandboxRegistry(publicClient, {
|
|
1135
|
+
* registryAddress,
|
|
1136
|
+
* chainId: 11155111,
|
|
1137
|
+
* from: investor,
|
|
1138
|
+
* });
|
|
1139
|
+
* if (!probe.attached) showError('This registry is not running the sandbox implementation.');
|
|
1140
|
+
* ```
|
|
1141
|
+
*
|
|
1142
|
+
* @throws {import('../errors.js').OpacityAttestationError} when `registryAddress`, `chainId`, or
|
|
1143
|
+
* `from` is malformed or zero.
|
|
1144
|
+
* @throws {import('../errors.js').OpacityConfigError} when the reader is connected to a different
|
|
1145
|
+
* chain than `chainId`.
|
|
1146
|
+
* @throws whatever the RPC client throws for a non-revert failure, untouched.
|
|
1147
|
+
*/
|
|
1148
|
+
declare function probeSandboxRegistry(client: SandboxProbeClient, params: ProbeSandboxRegistryParams): Promise<SandboxRegistryProbe>;
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Sepolia's chain id — the one chain the shared Opacity sandbox is deployed on today.
|
|
1152
|
+
*/
|
|
1153
|
+
declare const SEPOLIA_CHAIN_ID = 11155111;
|
|
1154
|
+
/**
|
|
1155
|
+
* The chain `client.sandbox` assumes when the client has no `chainId` configured and none is
|
|
1156
|
+
* passed per call. Sepolia, for now: it is the only chain with a sandbox deployment, so it is the
|
|
1157
|
+
* only honest default. `client.documents` deliberately does NOT inherit this default — the
|
|
1158
|
+
* attestation flow is chain-agnostic and refuses to guess.
|
|
1159
|
+
*/
|
|
1160
|
+
declare const SANDBOX_CHAIN_ID = 11155111;
|
|
1161
|
+
/**
|
|
1162
|
+
* The document/onboarding addresses of one sandbox deployment. Purchase vault/token/payment
|
|
1163
|
+
* addresses are separately pinned in `SANDBOX_PURCHASE_DEPLOYMENT`.
|
|
1164
|
+
*
|
|
1165
|
+
* The sandbox registry is upgradeable, so `allowlistRegistry` is the proxy address rather than
|
|
1166
|
+
* its current implementation. The Sepolia sandbox is a dedicated deployment initialized with
|
|
1167
|
+
* `SandboxAllowlistRegistry`; whether that implementation still answers is a live question — see
|
|
1168
|
+
* `probeSandboxRegistry`.
|
|
1169
|
+
*/
|
|
1170
|
+
interface SandboxDeployment {
|
|
1171
|
+
/** The chain these addresses live on. */
|
|
1172
|
+
chainId: number;
|
|
1173
|
+
/** The allowlist-registry PROXY running `SandboxAllowlistRegistry`. The target of `sandboxAddSelf()`. */
|
|
1174
|
+
allowlistRegistry: Address;
|
|
1175
|
+
/**
|
|
1176
|
+
* The `DocumentAttestor` wired as that registry's trait writer — the EIP-712 `verifyingContract`
|
|
1177
|
+
* of every session the sandbox backend issues. Pin it as `expectedAttestor` when you want the
|
|
1178
|
+
* wallet prompt refused if discovery ever disagrees.
|
|
1179
|
+
*/
|
|
1180
|
+
documentAttestor: Address;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Built-in sandbox deployments, keyed by chain id. Sepolia only, today.
|
|
1184
|
+
*
|
|
1185
|
+
* These and `SANDBOX_PURCHASE_DEPLOYMENT` are testnet-only constants; production addresses are
|
|
1186
|
+
* never baked in. They serve as
|
|
1187
|
+
* the FALLBACK for `client.sandbox` when discovery cannot answer, and as constants a consumer can
|
|
1188
|
+
* pin (`expectedAttestor`) or use with the pure builders without a backend at all.
|
|
1189
|
+
*
|
|
1190
|
+
* Source and receipt provenance: SEPOLIA_SANDBOX_MANIFEST and
|
|
1191
|
+
* deployments/sepolia-sandbox-v3.json. Verified sandboxVersion() = 3 after all
|
|
1192
|
+
* deployment and MMF seed transactions confirmed. Discovery remains backend-owned;
|
|
1193
|
+
* pin these addresses explicitly during the subgraph/backend cutover.
|
|
1194
|
+
*/
|
|
1195
|
+
declare const SANDBOX_DEPLOYMENTS: Readonly<Partial<Record<number, SandboxDeployment>>>;
|
|
1196
|
+
/** The built-in sandbox deployment for a chain, or `undefined` when the SDK ships none for it. */
|
|
1197
|
+
declare function getSandboxDeployment(chainId: number): SandboxDeployment | undefined;
|
|
1198
|
+
/**
|
|
1199
|
+
* The built-in sandbox deployment for a chain, or an error naming the chains that have one.
|
|
1200
|
+
*
|
|
1201
|
+
* @throws {OpacityConfigError} when no built-in deployment exists for `chainId`.
|
|
1202
|
+
*/
|
|
1203
|
+
declare function requireSandboxDeployment(chainId: number): SandboxDeployment;
|
|
1204
|
+
|
|
1205
|
+
/** One entry of an EIP-712 struct definition, as it arrives over the wire. */
|
|
1206
|
+
interface Eip712TypeField {
|
|
1207
|
+
name: string;
|
|
1208
|
+
type: string;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* The `typedData` block of a create response, exactly as the backend sends it.
|
|
1212
|
+
*
|
|
1213
|
+
* JSON-shaped: `deadline` is a decimal STRING because JSON cannot carry a `bigint`, and `types`
|
|
1214
|
+
* declares `EIP712Domain` alongside `DocumentAcceptance`.
|
|
1215
|
+
*
|
|
1216
|
+
* **Signable as it arrives on a raw `eth_signTypedData_v4` provider.** That method wants exactly
|
|
1217
|
+
* this JSON form, and mobile-api already declares `EIP712Domain` in `types`, so no reshaping is
|
|
1218
|
+
* needed. {@link toEip1193Json} / {@link toEip1193TypedData} remain worthwhile there for their
|
|
1219
|
+
* validation and attestor pinning, not because the payload needs repair.
|
|
1220
|
+
*
|
|
1221
|
+
* **viem and wagmi also take it as it arrives.** viem parses the decimal-string `uint256` to the
|
|
1222
|
+
* same digest a `bigint` produces, and ignores the extra `EIP712Domain` entry. Run it through
|
|
1223
|
+
* {@link toViemTypedData} anyway — for the validation and the `expectedAttestor` /
|
|
1224
|
+
* `expectedChainId` pinning, not for correctness.
|
|
1225
|
+
*
|
|
1226
|
+
* **ethers v6 is the one stack that cannot take it**, and that is the hard requirement
|
|
1227
|
+
* {@link toViemTypedData} exists to satisfy. `TypedDataEncoder` rejects a `types` map that declares
|
|
1228
|
+
* the domain, throwing `ambiguous primary types or unused types: "EIP712Domain",
|
|
1229
|
+
* "DocumentAcceptance"` — so the raw payload never reaches an ethers signer at all. Route it
|
|
1230
|
+
* through {@link toViemTypedData} (whose `types` is this SDK's constant, with no `EIP712Domain`
|
|
1231
|
+
* entry) and then `toEthersTypedData`.
|
|
1232
|
+
*/
|
|
1233
|
+
interface ServerAttestationTypedData {
|
|
1234
|
+
domain: {
|
|
1235
|
+
name: string;
|
|
1236
|
+
version: string;
|
|
1237
|
+
chainId: number;
|
|
1238
|
+
verifyingContract: Address;
|
|
1239
|
+
};
|
|
1240
|
+
types: Record<string, readonly Eip712TypeField[]>;
|
|
1241
|
+
primaryType: string;
|
|
1242
|
+
message: {
|
|
1243
|
+
nonce: Hex;
|
|
1244
|
+
docHash: Hex;
|
|
1245
|
+
/** Decimal string of unix seconds. NOT a number, and NOT a bigint. */
|
|
1246
|
+
deadline: string;
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
/** Optional domain pinning for {@link toViemTypedData}. */
|
|
1250
|
+
interface ToViemTypedDataOptions {
|
|
1251
|
+
/**
|
|
1252
|
+
* The `DocumentAttestor` address this client will accept, checked against
|
|
1253
|
+
* `typedData.domain.verifyingContract`.
|
|
1254
|
+
*
|
|
1255
|
+
* Supplying it is defence in depth and costs nothing: a client that blindly signs whatever
|
|
1256
|
+
* domain arrives over the wire has no independent anchor on which contract it is authorising, so
|
|
1257
|
+
* a compromised or merely misconfigured backend could aim a valid-looking prompt at a different
|
|
1258
|
+
* deployment. The investor cannot tell those apart; a pinned constant can.
|
|
1259
|
+
*/
|
|
1260
|
+
expectedAttestor?: Address;
|
|
1261
|
+
/** The chain id this client will accept, checked against `typedData.domain.chainId`. */
|
|
1262
|
+
expectedChainId?: number;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Validates the backend's JSON `typedData` and returns it in this SDK's own viem-shaped form — the
|
|
1266
|
+
* one `toEthersTypedData`, `toEip712JsonPayload` and `hashDocumentAcceptance` all take.
|
|
1267
|
+
*
|
|
1268
|
+
* **Only ethers strictly needs it, and not for the reason you would guess.** The `deadline`
|
|
1269
|
+
* reshaping is cosmetic: viem and ethers both hash the wire's decimal-string `uint256` to the same
|
|
1270
|
+
* digest as the `bigint` this returns. What ethers v6 will NOT accept is the `EIP712Domain` entry
|
|
1271
|
+
* mobile-api declares in `types` — `TypedDataEncoder` throws `ambiguous primary types or unused
|
|
1272
|
+
* types: "EIP712Domain", "DocumentAcceptance"` — so the server's payload cannot reach an ethers
|
|
1273
|
+
* signer at all. This function's `types` is the SDK's own constant, which omits that entry, which
|
|
1274
|
+
* is what makes the `toEthersTypedData` hand-off work.
|
|
1275
|
+
*
|
|
1276
|
+
* **viem, wagmi and a raw `eth_signTypedData_v4` provider all accept `session.typedData` as it
|
|
1277
|
+
* arrives.** viem tolerates both the decimal string and the extra `EIP712Domain` entry;
|
|
1278
|
+
* `eth_signTypedData_v4` is specified in terms of exactly that JSON shape. Use this function on
|
|
1279
|
+
* those paths for what it validates and pins, not because the payload needs repair — and use
|
|
1280
|
+
* {@link toEip1193Json} for the EIP-1193 one, which runs these same checks and re-serialises.
|
|
1281
|
+
*
|
|
1282
|
+
* Everything else this function does is validation, because the payload it is handed decides what
|
|
1283
|
+
* a human is about to authorise:
|
|
1284
|
+
*
|
|
1285
|
+
* - `primaryType` must be `DocumentAcceptance`.
|
|
1286
|
+
* - The `DocumentAcceptance` field list must match {@link DOCUMENT_ACCEPTANCE_TYPES} exactly —
|
|
1287
|
+
* same names, same types, same ORDER. Order is consensus-critical: it is part of the type hash,
|
|
1288
|
+
* and a reordered list produces a valid signature over a different struct. Unreferenced extra
|
|
1289
|
+
* entries in `types` are ignored: the encoding of this payload is fully determined by the primary
|
|
1290
|
+
* type's field list, all of which are atomic, so an unused extra type cannot change the digest.
|
|
1291
|
+
* - `EIP712Domain` is the one exception to that, and mobile-api declares it on EVERY session — so
|
|
1292
|
+
* this check runs on the normal path, not on a hypothetical future one. A declaration matching
|
|
1293
|
+
* {@link EIP712_DOMAIN_TYPE} is accepted; any other declaration — including the empty list — is
|
|
1294
|
+
* rejected, because that entry is not inert. It replaces the domain type a wallet would otherwise
|
|
1295
|
+
* derive, so a wrong one re-hashes the domain and moves the digest. An absent entry is tolerated
|
|
1296
|
+
* too, since viem and ethers derive the type themselves and this SDK re-declares it on the
|
|
1297
|
+
* EIP-1193 path.
|
|
1298
|
+
* - The domain `name`/`version` must be `DocumentAttestor`/`1`, and `chainId` /
|
|
1299
|
+
* `verifyingContract` must be well-formed.
|
|
1300
|
+
* - `nonce` and `docHash` must be well-formed `bytes32`, and `docHash` non-zero.
|
|
1301
|
+
*
|
|
1302
|
+
* Pass {@link ToViemTypedDataOptions} to additionally pin the domain to a deployment this client
|
|
1303
|
+
* already knows about. Optional, and worth taking: see the field docs for why blind signing leaves
|
|
1304
|
+
* the client with no independent anchor.
|
|
1305
|
+
*
|
|
1306
|
+
* The returned `types` object is this SDK's own constant rather than the server's array. That is
|
|
1307
|
+
* safe precisely because the check above proved the two are identical field for field, and it
|
|
1308
|
+
* means the result is the same `DocumentAcceptanceTypedData` the local builder produces — so
|
|
1309
|
+
* `toEthersTypedData`, `toEip712JsonPayload` and `hashDocumentAcceptance` all accept it.
|
|
1310
|
+
*
|
|
1311
|
+
* @throws {import('../errors.js').OpacityAttestationError} on any mismatch. Nothing is coerced
|
|
1312
|
+
* silently; a payload that does not validate must not reach a wallet.
|
|
1313
|
+
*/
|
|
1314
|
+
declare function toViemTypedData(typedData: ServerAttestationTypedData, options?: ToViemTypedDataOptions): DocumentAcceptanceTypedData;
|
|
1315
|
+
/**
|
|
1316
|
+
* Converts the backend's JSON `typedData` into a payload a raw EIP-1193 provider can sign, and
|
|
1317
|
+
* validates it on the way through.
|
|
1318
|
+
*
|
|
1319
|
+
* ```ts
|
|
1320
|
+
* const signature = await provider.request({
|
|
1321
|
+
* method: 'eth_signTypedData_v4',
|
|
1322
|
+
* params: [account, toEip1193Json(session.typedData, { expectedAttestor, expectedChainId })],
|
|
1323
|
+
* });
|
|
1324
|
+
* ```
|
|
1325
|
+
*
|
|
1326
|
+
* Sending `session.typedData` as it arrives is FINE — mobile-api declares `EIP712Domain`
|
|
1327
|
+
* alongside `DocumentAcceptance`, which is what makes it safe. This function is the belt-and-braces
|
|
1328
|
+
* route: it validates and pins the payload before a wallet ever sees it.
|
|
1329
|
+
*
|
|
1330
|
+
* What it guards against is a `types` map that declares `EIP712Domain` INCORRECTLY, or omits it.
|
|
1331
|
+
* `eth_signTypedData_v4` implementations built on MetaMask's `eth-sig-util` sanitise with
|
|
1332
|
+
* `{ EIP712Domain: [], ...types }`, so a missing entry becomes an EMPTY field list rather than an
|
|
1333
|
+
* error. The domain then hashes as `keccak256("EIP712Domain()")` — no `name`, no `version`, no
|
|
1334
|
+
* `chainId`, no `verifyingContract` — and the digest changes:
|
|
1335
|
+
*
|
|
1336
|
+
* ```text
|
|
1337
|
+
* EIP712Domain declared 0x91feb0709c4ebdff9ccc57b12b26c50936eae7973f67c6d23780fcfdbda3f410 ✓
|
|
1338
|
+
* EIP712Domain defaulted [] 0x82ada7044278f3b529058b2dac44bb987f078c23b15195e1dca77110f0c91691 ✗
|
|
1339
|
+
* ```
|
|
1340
|
+
*
|
|
1341
|
+
* Nothing throws. The wallet shows the right prompt and returns a well-formed signature over a
|
|
1342
|
+
* digest the contract has never heard of, and the failure surfaces much later as an
|
|
1343
|
+
* `InvalidUserSignature` from the relay's simulate step — which reads like "your wallet is
|
|
1344
|
+
* broken", not "the payload was missing a type". That is why {@link assertDomainFieldsIfDeclared}
|
|
1345
|
+
* rejects a MALFORMED `EIP712Domain` rather than quietly replacing it: the SDK will not hand a
|
|
1346
|
+
* wallet a payload whose domain type it cannot vouch for.
|
|
1347
|
+
*
|
|
1348
|
+
* The conversion runs {@link toViemTypedData} first, so every check listed there applies — the
|
|
1349
|
+
* primary type, the field list and its ORDER, the domain, and the optional
|
|
1350
|
+
* {@link ToViemTypedDataOptions} pinning — and then {@link toEip712JsonPayload} re-serialises with
|
|
1351
|
+
* `EIP712Domain` declared and `deadline` back to a decimal string. That round trip through a
|
|
1352
|
+
* `bigint` is deliberate: it is what proves the string the wallet receives is a well-formed
|
|
1353
|
+
* `uint256` rather than whatever the wire happened to carry.
|
|
1354
|
+
*
|
|
1355
|
+
* @throws {import('../errors.js').OpacityAttestationError} on any mismatch, exactly as
|
|
1356
|
+
* {@link toViemTypedData} does. A payload that does not validate must not reach a wallet.
|
|
1357
|
+
*/
|
|
1358
|
+
declare function toEip1193TypedData(typedData: ServerAttestationTypedData, options?: ToViemTypedDataOptions): Eip712JsonPayload;
|
|
1359
|
+
/**
|
|
1360
|
+
* {@link toEip1193TypedData}, serialised — the second `eth_signTypedData_v4` parameter.
|
|
1361
|
+
*
|
|
1362
|
+
* Providers differ on whether they want the object or the JSON string; the string is accepted
|
|
1363
|
+
* everywhere, so this is the safer default. Same validation, same reason for existing: see
|
|
1364
|
+
* {@link toEip1193TypedData} for the digest that silently changes without it.
|
|
1365
|
+
*
|
|
1366
|
+
* @throws {import('../errors.js').OpacityAttestationError} on any mismatch.
|
|
1367
|
+
*/
|
|
1368
|
+
declare function toEip1193Json(typedData: ServerAttestationTypedData, options?: ToViemTypedDataOptions): string;
|
|
1369
|
+
|
|
1370
|
+
export { type AttestationSessionResponseLike, BuildAttestDocumentTransactionParams, BuildDocumentAcceptanceTypedDataParams, type BuildSandboxAddSelfTransactionParams, type BuildSandboxRegisterIssuerTransactionParams, type BuildSandboxSetSelfTraitTransactionParams, ClockOptions, DOCUMENT_ACCEPTANCE_PRIMARY_TYPE, DOCUMENT_ACCEPTANCE_TYPEHASH, DOCUMENT_ACCEPTANCE_TYPES, DOCUMENT_ATTESTOR_DOMAIN_NAME, DOCUMENT_ATTESTOR_DOMAIN_VERSION, type DocumentAcceptanceMessage, type DocumentAcceptanceTypedData, type DocumentAttestorDomain, ECDSA_SIGNATURE_BYTES, EIP712_DOMAIN_TYPE, type Eip712JsonPayload, type Eip712TypeField, type EthersTypedData, type JsonRpcTransaction, MAX_ATTESTATION_LIFETIME_SECONDS, type ProbeSandboxRegistryParams, SANDBOX_CHAIN_ID, SANDBOX_DEPLOYMENTS, SANDBOX_IMPLEMENTATION_VERSION, SANDBOX_TRAIT_IDS, SEPOLIA_CHAIN_ID, type SandboxAddSelfWriteContractRequest, type SandboxDeployment, type SandboxProbeClient, type SandboxProbeEvidence, type SandboxRegisterIssuerWriteContractRequest, type SandboxRegistryProbe, type SandboxSetSelfTraitWriteContractRequest, type ServerAttestationTypedData, type ToViemTypedDataOptions, TransactionRequest, type VerifyAttestationSessionFields, type VerifyAttestationSessionParams, type VerifyAttestationSessionResponseParams, type WriteContractRequest, assertDocumentMatchesHash, buildAttestDocumentTransaction, buildDocumentAcceptanceTypedData, buildSandboxAddSelfTransaction, buildSandboxRegisterIssuerTransaction, buildSandboxSetSelfTraitTransaction, documentAttestorAbi, documentTraitId, getSandboxDeployment, hashDocument, hashDocumentAcceptance, probeSandboxRegistry, requireSandboxDeployment, sandboxAllowlistRegistryAbi, sandboxUserId, toEip1193Json, toEip1193TypedData, toEip712Json, toEip712JsonPayload, toEthersTypedData, toJsonRpcTransaction, toSandboxAddSelfWriteContractRequest, toSandboxRegisterIssuerWriteContractRequest, toSandboxSetSelfTraitWriteContractRequest, toViemTypedData, toWriteContractRequest, verifyAttestationSession };
|