@openzeppelin/miden-multisig-client 0.17.0 → 0.18.0-rc.2
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 +171 -46
- package/dist/account/builder.d.ts +4 -4
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +17 -7
- package/dist/account/builder.js.map +1 -1
- package/dist/account/layout.d.ts +5 -5
- package/dist/account/layout.d.ts.map +1 -1
- package/dist/account/layout.js +5 -5
- package/dist/account/layout.js.map +1 -1
- package/dist/client.d.ts +18 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +79 -6
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/multisig/authArgErrors.d.ts +28 -31
- package/dist/multisig/authArgErrors.d.ts.map +1 -1
- package/dist/multisig/authArgErrors.js +42 -46
- package/dist/multisig/authArgErrors.js.map +1 -1
- package/dist/multisig/consumeNotesErrors.d.ts +13 -1
- package/dist/multisig/consumeNotesErrors.d.ts.map +1 -1
- package/dist/multisig/consumeNotesErrors.js +17 -0
- package/dist/multisig/consumeNotesErrors.js.map +1 -1
- package/dist/multisig/signing.d.ts +1 -1
- package/dist/multisig/signing.d.ts.map +1 -1
- package/dist/multisig/signing.js +8 -3
- package/dist/multisig/signing.js.map +1 -1
- package/dist/multisig.d.ts +145 -18
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +493 -162
- package/dist/multisig.js.map +1 -1
- package/dist/procedures.d.ts +7 -7
- package/dist/procedures.js +7 -7
- package/dist/proposal/factory.d.ts.map +1 -1
- package/dist/proposal/factory.js +7 -0
- package/dist/proposal/factory.js.map +1 -1
- package/dist/prover/workflow.d.ts +7 -3
- package/dist/prover/workflow.d.ts.map +1 -1
- package/dist/prover/workflow.js +7 -5
- package/dist/prover/workflow.js.map +1 -1
- package/dist/raw-client.d.ts +1 -0
- package/dist/raw-client.d.ts.map +1 -1
- package/dist/raw-client.js +10 -2
- package/dist/raw-client.js.map +1 -1
- package/dist/recovery/publicNoteBackfill.js +1 -1
- package/dist/recovery/publicNoteBackfill.js.map +1 -1
- package/dist/retry/classify.d.ts +3 -0
- package/dist/retry/classify.d.ts.map +1 -1
- package/dist/retry/classify.js +2 -2
- package/dist/retry/classify.js.map +1 -1
- package/dist/signer.d.ts +1 -0
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +1 -0
- package/dist/signer.js.map +1 -1
- package/dist/signers/index.d.ts +1 -0
- package/dist/signers/index.d.ts.map +1 -1
- package/dist/signers/index.js +1 -0
- package/dist/signers/index.js.map +1 -1
- package/dist/signers/ledger.d.ts +25 -0
- package/dist/signers/ledger.d.ts.map +1 -0
- package/dist/signers/ledger.js +96 -0
- package/dist/signers/ledger.js.map +1 -0
- package/dist/state/adopt.d.ts +45 -0
- package/dist/state/adopt.d.ts.map +1 -0
- package/dist/state/adopt.js +101 -0
- package/dist/state/adopt.js.map +1 -0
- package/dist/transaction/authArgs.d.ts +57 -0
- package/dist/transaction/authArgs.d.ts.map +1 -0
- package/dist/transaction/authArgs.js +108 -0
- package/dist/transaction/authArgs.js.map +1 -0
- package/dist/transaction/consumeNotes.d.ts +9 -5
- package/dist/transaction/consumeNotes.d.ts.map +1 -1
- package/dist/transaction/consumeNotes.js +8 -23
- package/dist/transaction/consumeNotes.js.map +1 -1
- package/dist/transaction/noteAuthentication.d.ts +39 -0
- package/dist/transaction/noteAuthentication.d.ts.map +1 -0
- package/dist/transaction/noteAuthentication.js +94 -0
- package/dist/transaction/noteAuthentication.js.map +1 -0
- package/dist/transaction/options.d.ts +25 -0
- package/dist/transaction/options.d.ts.map +1 -1
- package/dist/transaction/p2id.d.ts +3 -2
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +36 -27
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/summary.d.ts +126 -22
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +164 -22
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction/updateGuardian.d.ts +3 -3
- package/dist/transaction/updateGuardian.d.ts.map +1 -1
- package/dist/transaction/updateGuardian.js +5 -16
- package/dist/transaction/updateGuardian.js.map +1 -1
- package/dist/transaction/updateProcedureThreshold.d.ts +3 -3
- package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
- package/dist/transaction/updateProcedureThreshold.js +6 -16
- package/dist/transaction/updateProcedureThreshold.js.map +1 -1
- package/dist/transaction/updateSigners.d.ts +3 -3
- package/dist/transaction/updateSigners.d.ts.map +1 -1
- package/dist/transaction/updateSigners.js +9 -16
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts +3 -2
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +3 -2
- package/dist/transaction.js.map +1 -1
- package/dist/types/proposal.d.ts +37 -5
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js +8 -0
- package/dist/types/proposal.js.map +1 -1
- package/dist/utils/eip712.d.ts +80 -0
- package/dist/utils/eip712.d.ts.map +1 -0
- package/dist/utils/eip712.js +49 -0
- package/dist/utils/eip712.js.map +1 -0
- package/dist/utils/signature.d.ts +4 -0
- package/dist/utils/signature.d.ts.map +1 -1
- package/dist/utils/signature.js +49 -1
- package/dist/utils/signature.js.map +1 -1
- package/package.json +11 -6
- package/src/account/builder.ts +18 -7
- package/src/account/layout.ts +5 -5
- package/src/client.ts +94 -6
- package/src/index.ts +24 -3
- package/src/multisig/authArgErrors.ts +47 -53
- package/src/multisig/consumeNotesErrors.ts +20 -1
- package/src/multisig/signing.ts +8 -2
- package/src/multisig.ts +614 -205
- package/src/procedures.ts +7 -7
- package/src/proposal/factory.ts +7 -0
- package/src/prover/workflow.ts +7 -10
- package/src/raw-client.ts +11 -7
- package/src/recovery/publicNoteBackfill.ts +1 -1
- package/src/retry/classify.ts +3 -3
- package/src/signer.ts +1 -0
- package/src/signers/index.ts +1 -0
- package/src/signers/ledger.ts +122 -0
- package/src/state/adopt.ts +132 -0
- package/src/transaction/authArgs.ts +142 -0
- package/src/transaction/consumeNotes.ts +23 -30
- package/src/transaction/noteAuthentication.ts +136 -0
- package/src/transaction/options.ts +27 -0
- package/src/transaction/p2id.ts +45 -34
- package/src/transaction/summary.ts +239 -30
- package/src/transaction/updateGuardian.ts +8 -22
- package/src/transaction/updateProcedureThreshold.ts +8 -20
- package/src/transaction/updateSigners.ts +11 -22
- package/src/transaction.ts +18 -1
- package/src/types/proposal.ts +36 -5
- package/src/utils/eip712.ts +57 -0
- package/src/utils/signature.ts +57 -0
- package/src/prover/test-node.d.ts +0 -6
package/src/procedures.ts
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* human-readable encoding and should not be copied into this table.
|
|
12
12
|
*/
|
|
13
13
|
export const PROCEDURE_ROOTS = {
|
|
14
|
-
update_signers: '
|
|
15
|
-
update_procedure_threshold: '
|
|
16
|
-
auth_tx: '
|
|
17
|
-
update_guardian: '
|
|
18
|
-
send_asset: '
|
|
19
|
-
receive_asset: '
|
|
14
|
+
update_signers: '0xe39b380d435dd42206fd625fcddfd26a379a2312cfef0271e9b5f18cbdec67e5',
|
|
15
|
+
update_procedure_threshold: '0x5de3563f30c5dd130da49c8fdd86d867fed6dbbd928363021c53ef99d8034bac',
|
|
16
|
+
auth_tx: '0xf988ff88c7a9c2104d77862d580239ec40e060a9b4d2d96028135abeb8cc58bc',
|
|
17
|
+
update_guardian: '0x93dedb135fd5bb7112c07aacf4a5680ddc76e45ecf043bfc42ea735b6d971911',
|
|
18
|
+
send_asset: '0x936e9920bffd7f458cc9ba2c4bbcc018fc4d3561511d79635955129268039dd7',
|
|
19
|
+
receive_asset: '0xd7416b798a70aabbca510c3cd0f48ba35473b5d76dc302375157c6f563fffc15',
|
|
20
20
|
} as const;
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -33,7 +33,7 @@ export type ProcedureName = keyof typeof PROCEDURE_ROOTS;
|
|
|
33
33
|
* @example
|
|
34
34
|
* ```typescript
|
|
35
35
|
* const root = getProcedureRoot('send_asset');
|
|
36
|
-
* // '
|
|
36
|
+
* // '0x936e9920bffd7f458cc9ba2c4bbcc018fc4d3561511d79635955129268039dd7'
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
export function getProcedureRoot(name: ProcedureName): string {
|
package/src/proposal/factory.ts
CHANGED
|
@@ -92,6 +92,7 @@ export class ProposalFactory {
|
|
|
92
92
|
txSummary: delta.deltaPayload.txSummary.data,
|
|
93
93
|
signatures,
|
|
94
94
|
metadata: resolvedMetadata,
|
|
95
|
+
verification: { status: 'unchecked' },
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -117,6 +118,10 @@ export class ProposalFactory {
|
|
|
117
118
|
`Invalid imported proposal signatures: ECDSA signature for ${signature.commitment} is missing publicKey`,
|
|
118
119
|
);
|
|
119
120
|
}
|
|
121
|
+
if (signature.messageFormat !== undefined &&
|
|
122
|
+
(scheme !== 'ecdsa' || signature.messageFormat !== 'eip712')) {
|
|
123
|
+
throw new Error('Invalid imported proposal signatures: unsupported message format');
|
|
124
|
+
}
|
|
120
125
|
|
|
121
126
|
return {
|
|
122
127
|
signerId: signature.commitment,
|
|
@@ -126,6 +131,7 @@ export class ProposalFactory {
|
|
|
126
131
|
scheme,
|
|
127
132
|
signature: signature.signatureHex,
|
|
128
133
|
publicKey: signature.publicKey,
|
|
134
|
+
...(signature.messageFormat ? { messageFormat: signature.messageFormat } : {}),
|
|
129
135
|
}
|
|
130
136
|
: {
|
|
131
137
|
scheme,
|
|
@@ -148,6 +154,7 @@ export class ProposalFactory {
|
|
|
148
154
|
txSummary: exported.txSummaryBase64,
|
|
149
155
|
signatures,
|
|
150
156
|
metadata,
|
|
157
|
+
verification: { status: 'unchecked' },
|
|
151
158
|
};
|
|
152
159
|
}
|
|
153
160
|
|
package/src/prover/workflow.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AccountId,
|
|
3
|
-
ChainAnchor,
|
|
4
3
|
MidenClient,
|
|
5
4
|
TransactionRequest,
|
|
6
5
|
} from '@miden-sdk/miden-sdk';
|
|
@@ -15,15 +14,13 @@ export class ProverWorkflow {
|
|
|
15
14
|
private readonly runtime?: RetryRuntime,
|
|
16
15
|
) {}
|
|
17
16
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
): Promise<void> {
|
|
24
|
-
const execution = await this.client.transactions.executeRequest(accountId, request
|
|
25
|
-
anchor,
|
|
26
|
-
});
|
|
17
|
+
/**
|
|
18
|
+
* Executes a request at the chain tip, then proves, submits, and applies it.
|
|
19
|
+
* A multisig proposal's request declares the block its summary binds, so the
|
|
20
|
+
* signed summary reproduces at the tip.
|
|
21
|
+
*/
|
|
22
|
+
async submit(accountId: AccountId, request: TransactionRequest): Promise<void> {
|
|
23
|
+
const execution = await this.client.transactions.executeRequest(accountId, request);
|
|
27
24
|
const proof = await proveWithRetry(execution, this.config, this.runtime);
|
|
28
25
|
const submission = await proof.submit();
|
|
29
26
|
await submission.apply();
|
package/src/raw-client.ts
CHANGED
|
@@ -29,7 +29,7 @@ export function requireMidenRpcEndpoint(endpoint?: string): string {
|
|
|
29
29
|
return requireConfigValue('midenRpcEndpoint', endpoint);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function isPublicMidenClient(client: RawClientSource): client is MidenClient {
|
|
32
|
+
export function isPublicMidenClient(client: RawClientSource): client is MidenClient {
|
|
33
33
|
return 'accounts' in client && 'sync' in client;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -47,16 +47,20 @@ export async function getRawMidenClient(
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const endpoint = requireMidenRpcEndpoint(rpcUrl);
|
|
50
|
-
const rawClient =
|
|
51
|
-
endpoint,
|
|
52
|
-
undefined,
|
|
53
|
-
undefined,
|
|
54
|
-
await client.storeIdentifier(),
|
|
55
|
-
);
|
|
50
|
+
const rawClient = createRawClient(client, endpoint);
|
|
56
51
|
rawClientCache.set(client, rawClient);
|
|
57
52
|
return rawClient;
|
|
58
53
|
}
|
|
59
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Opens the WASM client behind `client` on the same store. The protocol
|
|
57
|
+
* configuration comes from the node with every sync and is read from that
|
|
58
|
+
* shared store, so the shadow needs no fee faucet of its own.
|
|
59
|
+
*/
|
|
60
|
+
async function createRawClient(client: MidenClient, endpoint: string): Promise<WasmWebClient> {
|
|
61
|
+
return WasmWebClient.createClient(endpoint, undefined, undefined, await client.storeIdentifier());
|
|
62
|
+
}
|
|
63
|
+
|
|
60
64
|
export function getTransactionProver(client: RawClientSource): TransactionProver | null {
|
|
61
65
|
return isPublicMidenClient(client) ? client.defaultProver : null;
|
|
62
66
|
}
|
|
@@ -102,7 +102,7 @@ export function screenNoteForAccount(note: Note, account: AccountId): ScreenVerd
|
|
|
102
102
|
items[index].asInt() === suffix &&
|
|
103
103
|
items[index + 1].asInt() === prefix;
|
|
104
104
|
if (root === normalizeHexWord(NoteScript.p2id().root().toHex())) {
|
|
105
|
-
// P2ID note storage: [target.suffix, target.prefix].
|
|
105
|
+
// P2ID note storage: [target.suffix, target.prefix, salt_0, salt_1].
|
|
106
106
|
return accountAt(0) ? 'relevant' : 'irrelevant';
|
|
107
107
|
}
|
|
108
108
|
if (root === normalizeHexWord(NoteScript.p2ide().root().toHex())) {
|
package/src/retry/classify.ts
CHANGED
|
@@ -15,7 +15,7 @@ type ErrorRecord = {
|
|
|
15
15
|
message?: unknown;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
type StructuredEvidence = 'transient' | 'permanent' | 'indeterminate';
|
|
18
|
+
export type StructuredEvidence = 'transient' | 'permanent' | 'indeterminate';
|
|
19
19
|
|
|
20
20
|
const TRANSIENT_GRPC = new Set([
|
|
21
21
|
'cancelled',
|
|
@@ -96,7 +96,7 @@ function httpEvidence(value: unknown): StructuredEvidence | undefined {
|
|
|
96
96
|
return TRANSIENT_HTTP.has(value) ? 'transient' : 'permanent';
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function httpMessageEvidence(message: string): StructuredEvidence | undefined {
|
|
99
|
+
export function httpMessageEvidence(message: string): StructuredEvidence | undefined {
|
|
100
100
|
let hasTransient = false;
|
|
101
101
|
for (const match of message.matchAll(
|
|
102
102
|
/(?:\bhttp(?:\s+status)?|\bstatus)(?:\s+code)?\s*:?\s*(\d{3})\b/g,
|
|
@@ -110,7 +110,7 @@ function httpMessageEvidence(message: string): StructuredEvidence | undefined {
|
|
|
110
110
|
return hasTransient ? 'transient' : undefined;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function grpcMessageEvidence(message: string): StructuredEvidence | undefined {
|
|
113
|
+
export function grpcMessageEvidence(message: string): StructuredEvidence | undefined {
|
|
114
114
|
const normalized = message.replaceAll(/[^a-z0-9]/g, '');
|
|
115
115
|
let hasTransient = false;
|
|
116
116
|
for (const { token, evidence } of GRPC_MESSAGE_TOKENS) {
|
package/src/signer.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { FalconSigner } from './signers/falcon.js';
|
|
2
2
|
export { EcdsaSigner } from './signers/ecdsa.js';
|
|
3
|
+
export { Eip712Signer, LedgerSigner, type Eip1193SignerProvider } from './signers/ledger.js';
|
|
3
4
|
export { ParaSigner, type ParaSigningContext } from './signers/para.js';
|
|
4
5
|
export { MidenWalletSigner, type WalletSigningContext } from './signers/miden-wallet.js';
|
package/src/signers/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { FalconSigner } from './falcon.js';
|
|
2
2
|
export { EcdsaSigner } from './ecdsa.js';
|
|
3
|
+
export { Eip712Signer, LedgerSigner, type Eip1193SignerProvider } from './ledger.js';
|
|
3
4
|
export { ParaSigner, type ParaSigningContext } from './para.js';
|
|
4
5
|
export { MidenWalletSigner, type WalletSigningContext } from './miden-wallet.js';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
2
|
+
import { keccak_256 } from '@noble/hashes/sha3.js';
|
|
3
|
+
import type { RequestAuthPayload, Signer } from '@openzeppelin/guardian-client';
|
|
4
|
+
import { AuthDigest } from '../utils/digest.js';
|
|
5
|
+
import { lookupAuthDigest } from '../lookupAuth.js';
|
|
6
|
+
import { EcdsaFormat } from '../utils/ecdsa.js';
|
|
7
|
+
import { bytesToHex, hexToBytes } from '../utils/encoding.js';
|
|
8
|
+
import {
|
|
9
|
+
guardianKeyDiscoveryTypedData,
|
|
10
|
+
guardianLookupTypedData,
|
|
11
|
+
guardianRequestTypedData,
|
|
12
|
+
midenTransactionTypedData,
|
|
13
|
+
typedDataDigest,
|
|
14
|
+
} from '../utils/eip712.js';
|
|
15
|
+
import { tryComputeEcdsaCommitmentHex } from '../utils/signature.js';
|
|
16
|
+
import { wordToBytes } from '../utils/word.js';
|
|
17
|
+
|
|
18
|
+
export interface Eip1193SignerProvider {
|
|
19
|
+
request(args: { method: string; params: unknown[] }): Promise<unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Eip712Signer implements Signer {
|
|
23
|
+
readonly scheme = 'ecdsa';
|
|
24
|
+
readonly requestAuthFormat = 'eip712';
|
|
25
|
+
readonly proposalMessageFormat = 'eip712';
|
|
26
|
+
readonly publicKey: string;
|
|
27
|
+
readonly commitment: string;
|
|
28
|
+
readonly address: string;
|
|
29
|
+
|
|
30
|
+
static async connect(provider: Eip1193SignerProvider): Promise<Eip712Signer> {
|
|
31
|
+
const accounts = await provider.request({ method: 'eth_requestAccounts', params: [] });
|
|
32
|
+
if (!Array.isArray(accounts) || typeof accounts[0] !== 'string') {
|
|
33
|
+
throw new Error('Wallet did not provide an Ethereum address');
|
|
34
|
+
}
|
|
35
|
+
const address = accounts[0];
|
|
36
|
+
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
37
|
+
const data = guardianKeyDiscoveryTypedData(challenge);
|
|
38
|
+
const result = await provider.request({
|
|
39
|
+
method: 'eth_signTypedData_v4',
|
|
40
|
+
params: [address, JSON.stringify(data)],
|
|
41
|
+
});
|
|
42
|
+
if (typeof result !== 'string' || !/^0x[0-9a-fA-F]{130}$/.test(result)) {
|
|
43
|
+
throw new Error('Wallet returned an invalid key-discovery signature');
|
|
44
|
+
}
|
|
45
|
+
const signature = hexToBytes(EcdsaFormat.normalizeRecoveryByte(result));
|
|
46
|
+
if (signature[64] !== 0 && signature[64] !== 1) {
|
|
47
|
+
throw new Error('Wallet returned an invalid recovery ID');
|
|
48
|
+
}
|
|
49
|
+
const publicKey = secp256k1.Signature.fromCompact(signature.slice(0, 64))
|
|
50
|
+
.addRecoveryBit(signature[64])
|
|
51
|
+
.recoverPublicKey(typedDataDigest(data))
|
|
52
|
+
.toRawBytes(true);
|
|
53
|
+
return new Eip712Signer(provider, bytesToHex(publicKey), address);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
private readonly provider: Eip1193SignerProvider,
|
|
58
|
+
publicKey: string,
|
|
59
|
+
address: string,
|
|
60
|
+
) {
|
|
61
|
+
if (!EcdsaFormat.isValidPublicKeyPoint(publicKey)) {
|
|
62
|
+
throw new Error('Invalid wallet public key');
|
|
63
|
+
}
|
|
64
|
+
this.publicKey = EcdsaFormat.compressPublicKey(publicKey);
|
|
65
|
+
const commitment = tryComputeEcdsaCommitmentHex(this.publicKey);
|
|
66
|
+
if (!commitment) {
|
|
67
|
+
throw new Error('Cannot derive the Miden commitment for the wallet public key');
|
|
68
|
+
}
|
|
69
|
+
this.commitment = commitment;
|
|
70
|
+
|
|
71
|
+
const uncompressed = secp256k1.ProjectivePoint.fromHex(this.publicKey.slice(2)).toRawBytes(false);
|
|
72
|
+
const derivedAddress = bytesToHex(keccak_256(uncompressed.slice(1)).slice(-20));
|
|
73
|
+
if (derivedAddress.toLowerCase() !== address.toLowerCase()) {
|
|
74
|
+
throw new Error('Wallet address does not match its public key');
|
|
75
|
+
}
|
|
76
|
+
this.address = address;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
signAccountIdWithTimestamp(): Promise<string> {
|
|
80
|
+
throw new Error('Eip712Signer requires request-bound Guardian authentication');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async signRequest(
|
|
84
|
+
accountId: string,
|
|
85
|
+
timestamp: number,
|
|
86
|
+
requestPayload: RequestAuthPayload,
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const requestHash = AuthDigest.fromRequest(accountId, timestamp, requestPayload);
|
|
89
|
+
return this.signTypedData(guardianRequestTypedData(wordToBytes(requestHash)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async signLookupMessage(keyCommitmentHex: string, timestampMs: number): Promise<string> {
|
|
93
|
+
const lookupHash = lookupAuthDigest(timestampMs, keyCommitmentHex);
|
|
94
|
+
return this.signTypedData(guardianLookupTypedData(wordToBytes(lookupHash)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async signCommitment(commitmentHex: string): Promise<string> {
|
|
98
|
+
const commitment = AuthDigest.fromCommitmentHex(commitmentHex);
|
|
99
|
+
return this.signTypedData(midenTransactionTypedData(wordToBytes(commitment)));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async signTypedData(data: ReturnType<typeof guardianRequestTypedData>): Promise<string> {
|
|
103
|
+
const result = await this.provider.request({
|
|
104
|
+
method: 'eth_signTypedData_v4',
|
|
105
|
+
params: [this.address, JSON.stringify(data)],
|
|
106
|
+
});
|
|
107
|
+
if (typeof result !== 'string' || !/^0x[0-9a-fA-F]{130}$/.test(result)) {
|
|
108
|
+
throw new Error('Wallet returned an invalid EIP-712 signature');
|
|
109
|
+
}
|
|
110
|
+
const signatureHex = EcdsaFormat.normalizeRecoveryByte(result);
|
|
111
|
+
const signature = hexToBytes(signatureHex);
|
|
112
|
+
if (signature[64] !== 0 && signature[64] !== 1) {
|
|
113
|
+
throw new Error('Wallet returned an invalid ECDSA recovery ID');
|
|
114
|
+
}
|
|
115
|
+
if (!secp256k1.verify(signature.slice(0, 64), typedDataDigest(data), hexToBytes(this.publicKey))) {
|
|
116
|
+
throw new Error('Wallet signature was produced by a different key');
|
|
117
|
+
}
|
|
118
|
+
return signatureHex;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { Eip712Signer as LedgerSigner };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deciding when GUARDIAN-provided account state may replace the local store.
|
|
3
|
+
*
|
|
4
|
+
* Both entry points that pull an account from GUARDIAN need this: `syncState`,
|
|
5
|
+
* which refreshes an account already in hand, and `MultisigClient.load`, which
|
|
6
|
+
* rebuilds one. Sharing the rule is the point. Deciding it twice is how the two
|
|
7
|
+
* paths came to disagree, with `load` keeping whatever the store held whenever
|
|
8
|
+
* it held anything, so a caller that had touched the account before read stale
|
|
9
|
+
* state with no error.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Account, AccountId, Endpoint, RpcClient } from '@miden-sdk/miden-sdk';
|
|
13
|
+
|
|
14
|
+
import { retryRpcRead } from '../rpc/retry.js';
|
|
15
|
+
import type { ResolvedRpcConfig } from '../rpc/config.js';
|
|
16
|
+
import { normalizeHexWord } from '../utils/encoding.js';
|
|
17
|
+
|
|
18
|
+
const ZERO_COMMITMENT = `0x${'0'.repeat(64)}`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The account's commitment as the node reports it, or `null` when the account
|
|
22
|
+
* is not deployed yet. An undeployed account is not an error: it has no
|
|
23
|
+
* on-chain commitment to disagree with.
|
|
24
|
+
*/
|
|
25
|
+
export async function readOnChainCommitment(
|
|
26
|
+
midenRpcEndpoint: string,
|
|
27
|
+
accountId: AccountId,
|
|
28
|
+
rpcConfig: ResolvedRpcConfig,
|
|
29
|
+
): Promise<string | null> {
|
|
30
|
+
const rpcClient = new RpcClient(new Endpoint(midenRpcEndpoint));
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const accountDetails = await retryRpcRead(
|
|
34
|
+
() => rpcClient.getAccountDetails(accountId),
|
|
35
|
+
rpcConfig,
|
|
36
|
+
);
|
|
37
|
+
if (!accountDetails) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const commitment = normalizeHexWord(accountDetails.commitment().toHex());
|
|
41
|
+
return commitment === ZERO_COMMITMENT ? null : commitment;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
if (
|
|
45
|
+
message.includes('null pointer passed to rust') ||
|
|
46
|
+
message.includes('No account header record found for given ID') ||
|
|
47
|
+
message.toLowerCase().includes('not found')
|
|
48
|
+
) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decide whether GUARDIAN-provided state may overwrite the local store.
|
|
57
|
+
*
|
|
58
|
+
* Returns `false`, rather than throwing, when the GUARDIAN state is simply
|
|
59
|
+
* *behind* local (lower nonce). That happens whenever the execution delta the
|
|
60
|
+
* client pushed has not been canonicalized by GUARDIAN's background worker yet
|
|
61
|
+
* (see OpenZeppelin/guardian#316), or permanently if that candidate was
|
|
62
|
+
* discarded (#312 / #319). In that case the local account is already ahead and
|
|
63
|
+
* is independently verifiable against chain (`verifyStateCommitment`), so it is
|
|
64
|
+
* authoritative and must be kept, not clobbered; the caller keeps local and
|
|
65
|
+
* refreshes config from it.
|
|
66
|
+
*
|
|
67
|
+
* Still throws for genuine divergence: an incoming state at the *same* nonce as
|
|
68
|
+
* local but a different commitment, or an incoming state whose commitment does
|
|
69
|
+
* not match the on-chain commitment.
|
|
70
|
+
*
|
|
71
|
+
* Callers must only reach here once they know the two states differ. At equal
|
|
72
|
+
* nonce this treats the pair as divergent, which is true only when the
|
|
73
|
+
* commitments already disagree.
|
|
74
|
+
*/
|
|
75
|
+
export async function isSafeToAdoptGuardianState(params: {
|
|
76
|
+
accountId: string;
|
|
77
|
+
incomingAccount: Account;
|
|
78
|
+
localAccount?: Account;
|
|
79
|
+
readCommitment: () => Promise<string | null>;
|
|
80
|
+
}): Promise<boolean> {
|
|
81
|
+
const { accountId, incomingAccount, localAccount, readCommitment } = params;
|
|
82
|
+
|
|
83
|
+
if (localAccount) {
|
|
84
|
+
const localNonce = localAccount.nonce().asInt();
|
|
85
|
+
const incomingNonce = incomingAccount.nonce().asInt();
|
|
86
|
+
|
|
87
|
+
if (incomingNonce < localNonce) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (incomingNonce === localNonce) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Refusing to overwrite local state: incoming nonce ${incomingNonce.toString()} equals local nonce ${localNonce.toString()} but commitments differ for account ${accountId}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const onChainCommitment = await readCommitment();
|
|
99
|
+
if (!onChainCommitment) {
|
|
100
|
+
// No on-chain commitment means the account is not deployed, and an
|
|
101
|
+
// undeployed account has nothing to disagree with. That reading is only
|
|
102
|
+
// safe when the account has never transacted: `readOnChainCommitment`
|
|
103
|
+
// reports a missing account by matching `not found` in the error text, and
|
|
104
|
+
// a proxy or gateway 404 says exactly that. An account with a non-zero
|
|
105
|
+
// nonce has transacted, so it is deployed, so this is an RPC failure rather
|
|
106
|
+
// than an undeployed account, and adopting on it would skip the commitment
|
|
107
|
+
// check entirely.
|
|
108
|
+
// Keyed on the *local* nonce only. An account this client has transacted is
|
|
109
|
+
// deployed, so a node reporting nothing for it is an RPC failure rather
|
|
110
|
+
// than an undeployed account, and adopting there would skip the check
|
|
111
|
+
// against chain. The incoming nonce is GUARDIAN's claim rather than
|
|
112
|
+
// evidence, and `syncState` legitimately meets a higher incoming nonce with
|
|
113
|
+
// no local history; the caller that has no local record at all applies its
|
|
114
|
+
// own rule.
|
|
115
|
+
const transacted = localAccount?.nonce().asInt() ?? BigInt(0);
|
|
116
|
+
if (transacted > BigInt(0)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Refusing to overwrite local state: account ${accountId} has transacted (nonce ${transacted.toString()}) but the node reported no on-chain commitment, so the incoming state could not be checked against chain`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const incomingCommitment = normalizeHexWord(incomingAccount.to_commitment().toHex());
|
|
125
|
+
if (incomingCommitment !== onChainCommitment) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Refusing to overwrite local state: incoming commitment does not match on-chain commitment for account ${accountId}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Felt, TransactionRequest, TransactionRequestBuilder, Word } from '@miden-sdk/miden-sdk';
|
|
2
|
+
import { AccountId, Word as WordType } from '@miden-sdk/miden-sdk';
|
|
3
|
+
import { MultisigAuthArgsMissingError } from '../multisig/authArgErrors.js';
|
|
4
|
+
import { getRawMidenClient, isPublicMidenClient, type RawClientSource } from '../raw-client.js';
|
|
5
|
+
import { normalizeHexWord } from '../utils/encoding.js';
|
|
6
|
+
import { randomWord } from '../utils/random.js';
|
|
7
|
+
import type { MultisigRequestOptions } from './options.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Layout of the multisig auth-args preimage the auth arg commits to, in felts:
|
|
11
|
+
* `[bound_block_num, approval_expiration_block_num, 0, 0, SALT, CONVERSION_INFO]`.
|
|
12
|
+
*/
|
|
13
|
+
const AUTH_ARGS_BOUND_BLOCK_INDEX = 0;
|
|
14
|
+
const AUTH_ARGS_SALT_OFFSET = 4;
|
|
15
|
+
const AUTH_ARGS_NUM_ELEMENTS = 12;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A request under construction that already carries the multisig auth args,
|
|
19
|
+
* and the salt they commit to as normalized hex.
|
|
20
|
+
*/
|
|
21
|
+
export interface MultisigRequestDraft {
|
|
22
|
+
builder: TransactionRequestBuilder;
|
|
23
|
+
saltHex: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Starts a request carrying the multisig auth args for `options.accountId`:
|
|
28
|
+
* the three-word preimage in the advice map and its commitment as the auth
|
|
29
|
+
* arg, which miden-client then leaves alone.
|
|
30
|
+
*
|
|
31
|
+
* The salt is `options.salt` or a fresh one. `approvalExpirationDelta` left
|
|
32
|
+
* out means the approval never expires, the upstream default. `boundBlockNum`
|
|
33
|
+
* left out binds the store's sync height, which is what a proposer wants; a
|
|
34
|
+
* rebuild pins the proposal's anchor block and the expiration the summary
|
|
35
|
+
* already binds.
|
|
36
|
+
*
|
|
37
|
+
* The salt is moved across the WASM boundary, so a handle is built here from
|
|
38
|
+
* the hex rather than taken from the caller, and nothing frees it afterwards.
|
|
39
|
+
*/
|
|
40
|
+
export async function multisigRequestBuilder(
|
|
41
|
+
client: RawClientSource,
|
|
42
|
+
options: MultisigRequestOptions,
|
|
43
|
+
): Promise<MultisigRequestDraft> {
|
|
44
|
+
const { accountId, boundBlockNum, approvalExpirationDelta, midenRpcEndpoint } = options;
|
|
45
|
+
assertApprovalExpirationDelta(approvalExpirationDelta);
|
|
46
|
+
const saltHex = normalizeHexWord(options.salt ? options.salt.toHex() : randomWord().toHex());
|
|
47
|
+
const salt = WordType.fromHex(saltHex);
|
|
48
|
+
if (isPublicMidenClient(client)) {
|
|
49
|
+
const builder = await client.feeAwareTransactionRequestBuilder(accountId, {
|
|
50
|
+
feeConversionSalt: salt,
|
|
51
|
+
boundBlockNum,
|
|
52
|
+
approvalExpirationDelta,
|
|
53
|
+
});
|
|
54
|
+
return { builder, saltHex };
|
|
55
|
+
}
|
|
56
|
+
const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
|
|
57
|
+
const builder = await rawClient.feeAwareTransactionRequestBuilder(
|
|
58
|
+
AccountId.fromHex(accountId),
|
|
59
|
+
approvalExpirationDelta ?? null,
|
|
60
|
+
salt,
|
|
61
|
+
boundBlockNum ?? null,
|
|
62
|
+
);
|
|
63
|
+
return { builder, saltHex };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The furthest a transaction may expire after its reference block
|
|
68
|
+
* (`MAX_EXPIRATION_BLOCK_DELTA` in the transaction kernel). The auth procedure
|
|
69
|
+
* clamps the approval expiration it applies to this, so a longer approval would
|
|
70
|
+
* outlive the transaction it authorizes: the summary would still say "valid"
|
|
71
|
+
* while the node already refuses the submission as expired.
|
|
72
|
+
*/
|
|
73
|
+
export const MAX_APPROVAL_EXPIRATION_DELTA = 65_535;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Zero is not "no expiration": the kernel reads it as expired at the bound
|
|
77
|
+
* block and rejects the transaction. Refused here, where the caller can see why,
|
|
78
|
+
* together with a delta the auth procedure would silently clamp.
|
|
79
|
+
*/
|
|
80
|
+
function assertApprovalExpirationDelta(delta: number | undefined): void {
|
|
81
|
+
if (delta === undefined) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isInteger(delta) || delta < 1 || delta > MAX_APPROVAL_EXPIRATION_DELTA) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`approvalExpirationDelta must be a whole number of blocks between 1 and ${MAX_APPROVAL_EXPIRATION_DELTA}, ` +
|
|
87
|
+
`got ${delta}; omit it for an approval that does not expire`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Builds the request and hands back the salt it commits to as a fresh handle
|
|
94
|
+
* the caller owns. Refuses a request that carries no auth arg:
|
|
95
|
+
* `feeAwareTransactionRequestBuilder` hands back an untouched builder for an
|
|
96
|
+
* account it cannot classify as a multisig, typically one the client's store
|
|
97
|
+
* does not hold, and such a request would only fail later, inside the VM,
|
|
98
|
+
* while the auth procedure pipes a preimage that is not there.
|
|
99
|
+
*/
|
|
100
|
+
export function buildMultisigRequest(
|
|
101
|
+
builder: TransactionRequestBuilder,
|
|
102
|
+
saltHex: string,
|
|
103
|
+
accountId: string,
|
|
104
|
+
): { request: TransactionRequest; salt: Word } {
|
|
105
|
+
const request = builder.build();
|
|
106
|
+
if (!request.authArg()) {
|
|
107
|
+
throw new MultisigAuthArgsMissingError(accountId);
|
|
108
|
+
}
|
|
109
|
+
return { request, salt: WordType.fromHex(saltHex) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The block a multisig request's summary binds, read from the auth-args
|
|
114
|
+
* preimage the request carries. `undefined` when the request carries none.
|
|
115
|
+
*/
|
|
116
|
+
export function requestBoundBlockNum(request: TransactionRequest): number | undefined {
|
|
117
|
+
const preimage = authArgsPreimage(request);
|
|
118
|
+
return preimage ? Number(preimage[AUTH_ARGS_BOUND_BLOCK_INDEX].asInt()) : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The salt a multisig request's summary binds, read from the auth-args
|
|
123
|
+
* preimage the request carries. `undefined` when the request carries none.
|
|
124
|
+
*/
|
|
125
|
+
export function requestSaltHex(request: TransactionRequest): string | undefined {
|
|
126
|
+
const preimage = authArgsPreimage(request);
|
|
127
|
+
if (!preimage) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
return WordType.newFromFelts(
|
|
131
|
+
preimage.slice(AUTH_ARGS_SALT_OFFSET, AUTH_ARGS_SALT_OFFSET + 4),
|
|
132
|
+
).toHex();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function authArgsPreimage(request: TransactionRequest): Felt[] | undefined {
|
|
136
|
+
const authArg = request.authArg();
|
|
137
|
+
if (!authArg) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const felts = request.adviceMap().get(authArg);
|
|
141
|
+
return felts && felts.length === AUTH_ARGS_NUM_ELEMENTS ? felts : undefined;
|
|
142
|
+
}
|