@openzeppelin/miden-multisig-client 0.18.0-rc.3 → 0.18.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/README.md +117 -25
- package/dist/client.d.ts +39 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +77 -0
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/lookupAuth.d.ts.map +1 -1
- package/dist/lookupAuth.js +2 -20
- package/dist/lookupAuth.js.map +1 -1
- package/dist/multisig/authArgErrors.d.ts +27 -1
- package/dist/multisig/authArgErrors.d.ts.map +1 -1
- package/dist/multisig/authArgErrors.js +33 -0
- package/dist/multisig/authArgErrors.js.map +1 -1
- package/dist/multisig.d.ts +77 -11
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +247 -181
- package/dist/multisig.js.map +1 -1
- package/dist/proposal/metadata.d.ts.map +1 -1
- package/dist/proposal/metadata.js +2 -0
- package/dist/proposal/metadata.js.map +1 -1
- package/dist/recovery/transportDrain.d.ts +38 -30
- package/dist/recovery/transportDrain.d.ts.map +1 -1
- package/dist/recovery/transportDrain.js +113 -113
- package/dist/recovery/transportDrain.js.map +1 -1
- package/dist/session/grant.d.ts +40 -0
- package/dist/session/grant.d.ts.map +1 -0
- package/dist/session/grant.js +125 -0
- package/dist/session/grant.js.map +1 -0
- package/dist/session/key.d.ts +103 -0
- package/dist/session/key.d.ts.map +1 -0
- package/dist/session/key.js +160 -0
- package/dist/session/key.js.map +1 -0
- package/dist/session/manager.d.ts +76 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +187 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/signers/ecdsa.d.ts +5 -1
- package/dist/signers/ecdsa.d.ts.map +1 -1
- package/dist/signers/ecdsa.js +9 -0
- package/dist/signers/ecdsa.js.map +1 -1
- package/dist/signers/falcon.d.ts +5 -1
- package/dist/signers/falcon.d.ts.map +1 -1
- package/dist/signers/falcon.js +9 -0
- package/dist/signers/falcon.js.map +1 -1
- package/dist/signers/ledger.d.ts +9 -1
- package/dist/signers/ledger.d.ts.map +1 -1
- package/dist/signers/ledger.js +13 -1
- package/dist/signers/ledger.js.map +1 -1
- package/dist/signers/miden-wallet.d.ts +13 -1
- package/dist/signers/miden-wallet.d.ts.map +1 -1
- package/dist/signers/miden-wallet.js +25 -0
- package/dist/signers/miden-wallet.js.map +1 -1
- package/dist/signers/para.d.ts +5 -1
- package/dist/signers/para.d.ts.map +1 -1
- package/dist/signers/para.js +9 -0
- package/dist/signers/para.js.map +1 -1
- package/dist/transaction/authArgs.d.ts +2 -2
- package/dist/transaction/authArgs.js +2 -2
- package/dist/transaction/options.d.ts +2 -2
- package/dist/transaction/summary.d.ts +17 -61
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +49 -95
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction.d.ts +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +1 -1
- package/dist/transaction.js.map +1 -1
- package/dist/types/proposal.d.ts +11 -5
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js.map +1 -1
- package/dist/utils/eip712.d.ts +26 -69
- package/dist/utils/eip712.d.ts.map +1 -1
- package/dist/utils/eip712.js +101 -8
- package/dist/utils/eip712.js.map +1 -1
- package/dist/utils/felt.d.ts +7 -1
- package/dist/utils/felt.d.ts.map +1 -1
- package/dist/utils/felt.js +18 -1
- package/dist/utils/felt.js.map +1 -1
- package/package.json +4 -4
- package/src/client.ts +95 -0
- package/src/index.ts +27 -6
- package/src/lookupAuth.ts +3 -22
- package/src/multisig/authArgErrors.ts +42 -1
- package/src/multisig.ts +318 -207
- package/src/proposal/metadata.ts +2 -0
- package/src/recovery/transportDrain.ts +118 -124
- package/src/session/grant.ts +143 -0
- package/src/session/key.ts +215 -0
- package/src/session/manager.ts +273 -0
- package/src/signers/ecdsa.ts +12 -1
- package/src/signers/falcon.ts +12 -1
- package/src/signers/ledger.ts +19 -2
- package/src/signers/miden-wallet.ts +29 -1
- package/src/signers/para.ts +12 -1
- package/src/transaction/authArgs.ts +2 -2
- package/src/transaction/options.ts +2 -2
- package/src/transaction/summary.ts +62 -110
- package/src/transaction.ts +1 -6
- package/src/types/proposal.ts +11 -5
- package/src/utils/eip712.ts +130 -10
- package/src/utils/felt.ts +19 -1
|
@@ -8,8 +8,8 @@ export interface SignatureOptions {
|
|
|
8
8
|
/**
|
|
9
9
|
* The block the transaction summary binds. Omitted, the store's sync height,
|
|
10
10
|
* which is right for the party creating a proposal. A cosigner or executor
|
|
11
|
-
* rebuilding a proposal pins it to the proposal's
|
|
12
|
-
* summary can never match the signed one.
|
|
11
|
+
* rebuilding a proposal pins it to the block the proposal's summary binds, or
|
|
12
|
+
* the rebuilt summary can never match the signed one.
|
|
13
13
|
*/
|
|
14
14
|
boundBlockNum?: number;
|
|
15
15
|
/**
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { MidenClient, TransactionRequest, TransactionSummary } from '@miden-sdk/miden-sdk';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { Word } from '@miden-sdk/miden-sdk';
|
|
3
|
+
import {
|
|
4
|
+
BoundBlockNotDeclaredError,
|
|
5
|
+
TransactionSummaryLayoutError,
|
|
6
|
+
} from '../multisig/authArgErrors.js';
|
|
5
7
|
import { requestBoundBlockNum } from './authArgs.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -13,24 +15,16 @@ const APPROVAL_EXPIRATION_USER_PARAM_INDEX = 0;
|
|
|
13
15
|
const SALT_USER_PARAM_OFFSET = 2;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* The summary
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* The serialized summary layout {@link summaryBoundBlockNum} reads, from
|
|
19
|
+
* `TransactionSummary::write_into` at miden-protocol 0.17.0: the version byte
|
|
20
|
+
* first, then variable-length fields, then a fixed tail of the block number
|
|
21
|
+
* (u32, little endian), the block commitment (four 8-byte felts), the
|
|
22
|
+
* expiration delta (u16) and the six user-param felts, with no length prefix.
|
|
21
23
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
super(
|
|
27
|
-
`the transaction summary binds block commitment ${details.summaryBlockCommitmentHex} but ` +
|
|
28
|
-
`the captured chain anchor is ${details.anchorCommitmentHex}; a sync landed between ` +
|
|
29
|
-
'building the request and capturing its anchor, so rebuild the request and retry',
|
|
30
|
-
);
|
|
31
|
-
this.name = 'SummaryAnchorMismatchError';
|
|
32
|
-
}
|
|
33
|
-
}
|
|
24
|
+
const SUPPORTED_SUMMARY_VERSION = 1;
|
|
25
|
+
const BLOCK_NUMBER_BYTES = 4;
|
|
26
|
+
const BLOCK_COMMITMENT_BYTES = 32;
|
|
27
|
+
const SUMMARY_TAIL_BYTES = BLOCK_NUMBER_BYTES + BLOCK_COMMITMENT_BYTES + 2 + 6 * 8;
|
|
34
28
|
|
|
35
29
|
/**
|
|
36
30
|
* The Miden client synced and its node still has not produced the block a
|
|
@@ -67,57 +61,18 @@ export function isStaleChainError(error: unknown): boolean {
|
|
|
67
61
|
return message.includes('has been pruned');
|
|
68
62
|
}
|
|
69
63
|
|
|
70
|
-
/**
|
|
71
|
-
* Derives the summary awaiting authorization for a proposal the caller is
|
|
72
|
-
* creating now, and captures a `ChainAnchor` at the current sync height to ship
|
|
73
|
-
* with it.
|
|
74
|
-
*
|
|
75
|
-
* The summary is derived at the chain tip, like every other execution of a
|
|
76
|
-
* multisig proposal (see {@link executeForSummaryAtTip}). The anchor still
|
|
77
|
-
* travels in the proposal: it names the block the request's auth args bind,
|
|
78
|
-
* which is how a rebuild learns that block, and 0.18.0-rc.1 clients re-execute
|
|
79
|
-
* at it. A proposer builds at the sync height the anchor is captured at, and
|
|
80
|
-
* the check below is what makes that hold.
|
|
81
|
-
*/
|
|
82
|
-
export async function executeForSummary(
|
|
83
|
-
client: MidenClient,
|
|
84
|
-
accountId: string,
|
|
85
|
-
txRequest: TransactionRequest,
|
|
86
|
-
): Promise<{ summary: TransactionSummary; anchor: ChainAnchor }> {
|
|
87
|
-
const anchor = await client.transactions.captureAnchor(txRequest);
|
|
88
|
-
let summary: TransactionSummary;
|
|
89
|
-
try {
|
|
90
|
-
summary = await executeForSummaryAtTip(client, accountId, txRequest);
|
|
91
|
-
} catch (error) {
|
|
92
|
-
anchor.free();
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const anchorCommitment = anchor.commitment();
|
|
97
|
-
const summaryBlockCommitment = summary.blockCommitment();
|
|
98
|
-
const anchorCommitmentHex = normalizeHexWord(anchorCommitment.toHex());
|
|
99
|
-
const summaryBlockCommitmentHex = normalizeHexWord(summaryBlockCommitment.toHex());
|
|
100
|
-
anchorCommitment.free?.();
|
|
101
|
-
summaryBlockCommitment.free?.();
|
|
102
|
-
if (anchorCommitmentHex !== summaryBlockCommitmentHex) {
|
|
103
|
-
anchor.free();
|
|
104
|
-
throw new SummaryAnchorMismatchError({ anchorCommitmentHex, summaryBlockCommitmentHex });
|
|
105
|
-
}
|
|
106
|
-
return { summary, anchor };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
64
|
/**
|
|
110
65
|
* Executes a multisig request at the chain tip to obtain the summary awaiting
|
|
111
|
-
* authorization.
|
|
112
|
-
*
|
|
66
|
+
* authorization. Proposers derive a new proposal's summary with it, and
|
|
67
|
+
* cosigners and the executor reproduce it, whatever block they have synced to.
|
|
113
68
|
*
|
|
114
69
|
* Since protocol 0.17 a multisig summary binds the block its auth args name
|
|
115
70
|
* (the bound block), not the block the transaction executes against, so it
|
|
116
71
|
* reproduces at any later tip once the bound block is in the transaction's
|
|
117
72
|
* partial blockchain. The request declares it through `withBlockNumbers`, and
|
|
118
|
-
* foreign accounts, the fee faucet among them, load at the tip.
|
|
119
|
-
* the
|
|
120
|
-
*
|
|
73
|
+
* foreign accounts, the fee faucet among them, load at the tip. Executing at
|
|
74
|
+
* the bound block instead would load them there, which a node prunes about 50
|
|
75
|
+
* blocks later (issue #462).
|
|
121
76
|
*
|
|
122
77
|
* The client has to have synced to at least the bound block. When it has not,
|
|
123
78
|
* this syncs once before executing.
|
|
@@ -203,58 +158,55 @@ export async function syncToBoundBlock(
|
|
|
203
158
|
}
|
|
204
159
|
|
|
205
160
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
161
|
+
* Reads the block a multisig transaction summary binds, the counterpart of the
|
|
162
|
+
* Rust `TransactionSummary::block_number`. Since protocol 0.17 that is the block
|
|
163
|
+
* the request's multisig auth args name, so it is the proposal's bound block.
|
|
208
164
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
accountId: string,
|
|
217
|
-
txRequest: TransactionRequest,
|
|
218
|
-
anchor: ChainAnchor,
|
|
219
|
-
): Promise<TransactionSummary> {
|
|
220
|
-
return client.transactions.preview({
|
|
221
|
-
operation: 'custom',
|
|
222
|
-
account: accountId,
|
|
223
|
-
request: txRequest,
|
|
224
|
-
anchor,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Serializes a `ChainAnchor` to base64 for the proposal wire payload.
|
|
165
|
+
* The web SDK exposes no accessor for it, so this reads the serialized summary
|
|
166
|
+
* (layout above) and checks the block commitment next to it against
|
|
167
|
+
* `blockCommitment()`, so a layout this client does not know is refused rather
|
|
168
|
+
* than read at the wrong offset.
|
|
169
|
+
*
|
|
170
|
+
* @throws TransactionSummaryLayoutError when the summary is not version 1, is
|
|
171
|
+
* too short to hold the tail, or its tail does not hold its block commitment.
|
|
230
172
|
*/
|
|
231
|
-
export function
|
|
232
|
-
|
|
173
|
+
export function summaryBoundBlockNum(summary: TransactionSummary): number {
|
|
174
|
+
const bytes = summary.serialize();
|
|
175
|
+
const blockCommitment = summary.blockCommitment();
|
|
176
|
+
try {
|
|
177
|
+
return readBoundBlockNum(bytes, blockCommitment.serialize());
|
|
178
|
+
} finally {
|
|
179
|
+
blockCommitment.free();
|
|
180
|
+
}
|
|
233
181
|
}
|
|
234
182
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
183
|
+
function readBoundBlockNum(bytes: Uint8Array, blockCommitment: Uint8Array): number {
|
|
184
|
+
if (bytes.length < 1 + SUMMARY_TAIL_BYTES) {
|
|
185
|
+
throw new TransactionSummaryLayoutError(
|
|
186
|
+
`${bytes.length} bytes is shorter than the version byte and the ${SUMMARY_TAIL_BYTES}-byte tail`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (bytes[0] !== SUPPORTED_SUMMARY_VERSION) {
|
|
190
|
+
throw new TransactionSummaryLayoutError(
|
|
191
|
+
`version ${bytes[0]}, but only version ${SUPPORTED_SUMMARY_VERSION} is supported`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const blockNumberOffset = bytes.length - SUMMARY_TAIL_BYTES;
|
|
195
|
+
const commitmentOffset = blockNumberOffset + BLOCK_NUMBER_BYTES;
|
|
196
|
+
const tailCommitment = bytes.subarray(commitmentOffset, commitmentOffset + BLOCK_COMMITMENT_BYTES);
|
|
197
|
+
if (!bytesEqual(tailCommitment, blockCommitment)) {
|
|
198
|
+
throw new TransactionSummaryLayoutError(
|
|
199
|
+
'the bytes after the block number are not the summary block commitment',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(
|
|
203
|
+
blockNumberOffset,
|
|
204
|
+
true,
|
|
205
|
+
);
|
|
244
206
|
}
|
|
245
207
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
* binds: a custom producer rebuilds its request at this block. Decodes the
|
|
249
|
-
* anchor for the one number and frees it.
|
|
250
|
-
*/
|
|
251
|
-
export function chainAnchorBlockNum(anchorBase64: string): number {
|
|
252
|
-
const anchor = chainAnchorFromBase64(anchorBase64);
|
|
253
|
-
try {
|
|
254
|
-
return anchor.blockNum();
|
|
255
|
-
} finally {
|
|
256
|
-
anchor.free();
|
|
257
|
-
}
|
|
208
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
209
|
+
return a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
258
210
|
}
|
|
259
211
|
|
|
260
212
|
/**
|
package/src/transaction.ts
CHANGED
|
@@ -11,19 +11,14 @@ export {
|
|
|
11
11
|
} from './transaction/consumeNotes.js';
|
|
12
12
|
export {
|
|
13
13
|
ChainBehindBoundBlockError,
|
|
14
|
-
chainAnchorBlockNum,
|
|
15
|
-
chainAnchorFromBase64,
|
|
16
|
-
chainAnchorToBase64,
|
|
17
|
-
executeForSummary,
|
|
18
|
-
executeForSummaryAt,
|
|
19
14
|
executeForSummaryAtTip,
|
|
20
15
|
prepareTipExecution,
|
|
21
16
|
isStaleChainError,
|
|
22
17
|
requireDeclaredBoundBlock,
|
|
23
18
|
syncToBoundBlock,
|
|
24
19
|
summaryApprovalExpirationBlockNum,
|
|
20
|
+
summaryBoundBlockNum,
|
|
25
21
|
summarySalt,
|
|
26
|
-
SummaryAnchorMismatchError,
|
|
27
22
|
} from './transaction/summary.js';
|
|
28
23
|
export {
|
|
29
24
|
buildP2idNoteFromMetadata,
|
package/src/types/proposal.ts
CHANGED
|
@@ -38,11 +38,17 @@ interface BaseProposalMetadata {
|
|
|
38
38
|
saltHex?: string;
|
|
39
39
|
requiredSignatures?: number;
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* The block the proposal's transaction summary binds: the proposer's sync
|
|
42
|
+
* height when it built the request, or the block a custom producer's request
|
|
43
|
+
* names. Not signed: this SDK reads the block from the summary and refuses a
|
|
44
|
+
* proposal whose value disagrees (`BoundBlockMismatchError`), as the Rust SDK
|
|
45
|
+
* does. Every proposal this SDK creates writes it.
|
|
46
|
+
*/
|
|
47
|
+
boundBlockNum?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Legacy: the base64 Miden `ChainAnchor` 0.18 clients wrote instead of
|
|
50
|
+
* `boundBlockNum`. This SDK never sets or reads it; the codec passes it
|
|
51
|
+
* through so an old proposal exports and imports unchanged.
|
|
46
52
|
*/
|
|
47
53
|
chainAnchor?: string;
|
|
48
54
|
}
|
package/src/utils/eip712.ts
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
import { keccak_256 } from '@noble/hashes/sha3.js';
|
|
2
|
+
import type { SessionGrantFields } from '@openzeppelin/guardian-client';
|
|
2
3
|
import { bytesToHex, hexToBytes } from './encoding.js';
|
|
4
|
+
import { SESSION_GRANT_SCOPE, formatUtcSeconds } from '../session/grant.js';
|
|
3
5
|
|
|
4
6
|
type TypedField = { name: string; type: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* EIP-712 typed data over `string`, `bytes`, `bytes32` and `uint64` fields,
|
|
10
|
+
* as Guardian signs it. Integers are decimal strings, as wallets accept them.
|
|
11
|
+
*/
|
|
12
|
+
export interface GuardianTypedData {
|
|
13
|
+
types: Record<string, TypedField[]>;
|
|
14
|
+
primaryType: string;
|
|
15
|
+
domain: { name: string; version: string };
|
|
16
|
+
message: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
const EIP191_PREFIX = 0x19;
|
|
6
20
|
const EIP712_VERSION = 0x01;
|
|
7
21
|
const DOMAIN_VERSION = '1';
|
|
22
|
+
const SESSION_DOMAIN_NAME = 'Guardian Session';
|
|
23
|
+
const UINT64_MAX = 0xffffffffffffffffn;
|
|
8
24
|
|
|
9
25
|
const domainFields: TypedField[] = [
|
|
10
26
|
{ name: 'name', type: 'string' },
|
|
11
27
|
{ name: 'version', type: 'string' },
|
|
12
28
|
];
|
|
13
29
|
|
|
14
|
-
function typedData(
|
|
30
|
+
function typedData(
|
|
31
|
+
name: string,
|
|
32
|
+
primaryType: string,
|
|
33
|
+
fieldName: string,
|
|
34
|
+
value: string,
|
|
35
|
+
): GuardianTypedData {
|
|
15
36
|
return {
|
|
16
37
|
types: {
|
|
17
38
|
EIP712Domain: domainFields,
|
|
@@ -39,16 +60,115 @@ export function guardianKeyDiscoveryTypedData(challenge: Uint8Array) {
|
|
|
39
60
|
return typedData('Guardian Key Discovery', 'GuardianKeyDiscovery', 'challenge', bytesToHex(challenge));
|
|
40
61
|
}
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Readable session grant: every value Guardian binds is a top-level member,
|
|
65
|
+
* so the wallet displays the signer, the delegated key, the website, the
|
|
66
|
+
* lifetime (with a readable expiry), the scope, the Guardian key and the
|
|
67
|
+
* network instead of a hash.
|
|
68
|
+
*/
|
|
69
|
+
export function guardianSessionTypedData(grant: SessionGrantFields): GuardianTypedData {
|
|
70
|
+
return {
|
|
71
|
+
types: {
|
|
72
|
+
EIP712Domain: domainFields,
|
|
73
|
+
GuardianSession: [
|
|
74
|
+
{ name: 'signer', type: 'bytes32' },
|
|
75
|
+
{ name: 'sessionKey', type: 'bytes' },
|
|
76
|
+
{ name: 'origin', type: 'string' },
|
|
77
|
+
{ name: 'issuedAt', type: 'uint64' },
|
|
78
|
+
{ name: 'expiresAt', type: 'uint64' },
|
|
79
|
+
{ name: 'expires', type: 'string' },
|
|
80
|
+
{ name: 'scope', type: 'string' },
|
|
81
|
+
{ name: 'guardianKey', type: 'bytes32' },
|
|
82
|
+
{ name: 'network', type: 'string' },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
primaryType: 'GuardianSession',
|
|
86
|
+
domain: { name: SESSION_DOMAIN_NAME, version: DOMAIN_VERSION },
|
|
87
|
+
message: {
|
|
88
|
+
signer: grant.signerCommitment,
|
|
89
|
+
sessionKey: grant.sessionPublicKey,
|
|
90
|
+
origin: grant.origin,
|
|
91
|
+
issuedAt: String(grant.issuedAt),
|
|
92
|
+
expiresAt: String(grant.expiresAt),
|
|
93
|
+
expires: formatUtcSeconds(grant.expiresAt),
|
|
94
|
+
scope: SESSION_GRANT_SCOPE,
|
|
95
|
+
guardianKey: grant.guardianCommitment,
|
|
96
|
+
network: grant.network,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Readable revoke-all: the signer whose sessions end and the request
|
|
103
|
+
* timestamp in milliseconds.
|
|
104
|
+
*/
|
|
105
|
+
export function guardianSessionRevokeAllTypedData(
|
|
106
|
+
signerCommitment: string,
|
|
107
|
+
timestampMs: number,
|
|
108
|
+
): GuardianTypedData {
|
|
109
|
+
if (!Number.isInteger(timestampMs) || timestampMs < 0) {
|
|
110
|
+
throw new Error('Revoke-all timestamp must be a non-negative integer');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
types: {
|
|
114
|
+
EIP712Domain: domainFields,
|
|
115
|
+
GuardianSessionRevokeAll: [
|
|
116
|
+
{ name: 'signer', type: 'bytes32' },
|
|
117
|
+
{ name: 'timestamp', type: 'uint64' },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
primaryType: 'GuardianSessionRevokeAll',
|
|
121
|
+
domain: { name: SESSION_DOMAIN_NAME, version: DOMAIN_VERSION },
|
|
122
|
+
message: { signer: signerCommitment, timestamp: String(timestampMs) },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function encodeField(field: TypedField, value: string): Uint8Array {
|
|
127
|
+
switch (field.type) {
|
|
128
|
+
case 'string':
|
|
129
|
+
return keccak_256(new TextEncoder().encode(value));
|
|
130
|
+
case 'bytes':
|
|
131
|
+
return keccak_256(hexToBytes(value));
|
|
132
|
+
case 'bytes32': {
|
|
133
|
+
const bytes = hexToBytes(value);
|
|
134
|
+
if (bytes.length !== 32) {
|
|
135
|
+
throw new Error(`EIP-712 field ${field.name} must be 32 bytes`);
|
|
136
|
+
}
|
|
137
|
+
return bytes;
|
|
138
|
+
}
|
|
139
|
+
case 'uint64': {
|
|
140
|
+
let remaining = BigInt(value);
|
|
141
|
+
if (remaining < 0n || remaining > UINT64_MAX) {
|
|
142
|
+
throw new Error(`EIP-712 field ${field.name} must fit in uint64`);
|
|
143
|
+
}
|
|
144
|
+
const bytes = new Uint8Array(32);
|
|
145
|
+
for (let i = 31; i >= 24; i -= 1) {
|
|
146
|
+
bytes[i] = Number(remaining & 0xffn);
|
|
147
|
+
remaining >>= 8n;
|
|
148
|
+
}
|
|
149
|
+
return bytes;
|
|
150
|
+
}
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`Unsupported EIP-712 field type: ${field.type}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function typedDataDigest(data: GuardianTypedData): Uint8Array {
|
|
157
|
+
const encoder = new TextEncoder();
|
|
158
|
+
const fields = data.types[data.primaryType];
|
|
159
|
+
const typeString = `${data.primaryType}(${fields.map((f) => `${f.type} ${f.name}`).join(',')})`;
|
|
160
|
+
|
|
45
161
|
const domain = new Uint8Array(96);
|
|
46
|
-
domain.set(keccak_256(
|
|
47
|
-
domain.set(keccak_256(
|
|
48
|
-
domain.set(keccak_256(
|
|
49
|
-
|
|
50
|
-
struct
|
|
51
|
-
struct.set(
|
|
162
|
+
domain.set(keccak_256(encoder.encode('EIP712Domain(string name,string version)')));
|
|
163
|
+
domain.set(keccak_256(encoder.encode(data.domain.name)), 32);
|
|
164
|
+
domain.set(keccak_256(encoder.encode(data.domain.version)), 64);
|
|
165
|
+
|
|
166
|
+
const struct = new Uint8Array(32 * (fields.length + 1));
|
|
167
|
+
struct.set(keccak_256(encoder.encode(typeString)));
|
|
168
|
+
fields.forEach((field, index) => {
|
|
169
|
+
struct.set(encodeField(field, data.message[field.name]), 32 * (index + 1));
|
|
170
|
+
});
|
|
171
|
+
|
|
52
172
|
const preimage = new Uint8Array(66);
|
|
53
173
|
preimage.set([EIP191_PREFIX, EIP712_VERSION]);
|
|
54
174
|
preimage.set(keccak_256(domain), 2);
|
package/src/utils/felt.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Felt } from '@miden-sdk/miden-sdk';
|
|
1
|
+
import { Felt, FeltArray, Rpo256, type Word } from '@miden-sdk/miden-sdk';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Goldilocks field order (`2^64 - 2^32 + 1`), matching `Felt::ORDER` in
|
|
@@ -18,3 +18,21 @@ const FELT_ORDER = 18446744069414584321n;
|
|
|
18
18
|
export function feltFromU64Reduced(value: bigint): Felt {
|
|
19
19
|
return new Felt(BigInt.asUintN(64, value) % FELT_ORDER);
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* RPO hash of `bytes` packed as 8-byte little-endian chunks (the last one
|
|
24
|
+
* zero-padded), mirroring `guardian_shared::felt::domain_tag_word`. The
|
|
25
|
+
* convention every Guardian message domain tag uses.
|
|
26
|
+
*/
|
|
27
|
+
export function domainTagWord(bytes: Uint8Array): Word {
|
|
28
|
+
const felts: Felt[] = [];
|
|
29
|
+
for (let offset = 0; offset < bytes.length; offset += 8) {
|
|
30
|
+
let value = 0n;
|
|
31
|
+
for (let i = 0; i < 8; i += 1) {
|
|
32
|
+
const byte = offset + i < bytes.length ? bytes[offset + i] : 0;
|
|
33
|
+
value |= BigInt(byte) << BigInt(8 * i);
|
|
34
|
+
}
|
|
35
|
+
felts.push(feltFromU64Reduced(value));
|
|
36
|
+
}
|
|
37
|
+
return Rpo256.hashElements(new FeltArray(felts));
|
|
38
|
+
}
|