@unlink-xyz/core 0.1.0 → 0.1.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/.eslintrc.json +4 -0
- package/account/zkAccount.test.ts +316 -0
- package/account/zkAccount.ts +222 -0
- package/clients/broadcaster.ts +67 -0
- package/clients/http.ts +94 -0
- package/clients/indexer.ts +150 -0
- package/config.ts +39 -0
- package/core.ts +17 -0
- package/dist/account/railgun-imports-prototype.d.ts +12 -0
- package/dist/account/railgun-imports-prototype.d.ts.map +1 -0
- package/dist/account/railgun-imports-prototype.js +30 -0
- package/dist/clients/indexer.d.ts.map +1 -1
- package/dist/clients/indexer.js +1 -1
- package/dist/state/hydrator.d.ts +16 -0
- package/dist/state/hydrator.d.ts.map +1 -0
- package/dist/state/hydrator.js +18 -0
- package/dist/state/job-store.d.ts +12 -0
- package/dist/state/job-store.d.ts.map +1 -0
- package/dist/state/job-store.js +118 -0
- package/dist/state/jobs.d.ts +50 -0
- package/dist/state/jobs.d.ts.map +1 -0
- package/dist/state/jobs.js +1 -0
- package/dist/state.d.ts +83 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +171 -0
- package/dist/transactions/deposit.d.ts +0 -2
- package/dist/transactions/deposit.d.ts.map +1 -1
- package/dist/transactions/deposit.js +5 -9
- package/dist/transactions/note-sync.d.ts.map +1 -1
- package/dist/transactions/note-sync.js +1 -1
- package/dist/transactions/shield.d.ts +5 -0
- package/dist/transactions/shield.d.ts.map +1 -0
- package/dist/transactions/shield.js +93 -0
- package/dist/transactions/transact.d.ts +0 -5
- package/dist/transactions/transact.d.ts.map +1 -1
- package/dist/transactions/transact.js +2 -2
- package/dist/transactions/utils.d.ts +10 -0
- package/dist/transactions/utils.d.ts.map +1 -0
- package/dist/transactions/utils.js +17 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/time.d.ts +2 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +3 -0
- package/dist/utils/witness.d.ts +11 -0
- package/dist/utils/witness.d.ts.map +1 -0
- package/dist/utils/witness.js +19 -0
- package/errors.ts +20 -0
- package/index.ts +17 -0
- package/key-derivation/babyjubjub.ts +11 -0
- package/key-derivation/bech32.test.ts +90 -0
- package/key-derivation/bech32.ts +124 -0
- package/key-derivation/bip32.ts +56 -0
- package/key-derivation/bip39.ts +76 -0
- package/key-derivation/bytes.ts +118 -0
- package/key-derivation/hash.ts +13 -0
- package/key-derivation/index.ts +7 -0
- package/key-derivation/wallet-node.ts +155 -0
- package/keys.ts +47 -0
- package/package.json +4 -5
- package/prover/config.ts +104 -0
- package/prover/index.ts +1 -0
- package/prover/prover.integration.test.ts +162 -0
- package/prover/prover.test.ts +309 -0
- package/prover/prover.ts +405 -0
- package/prover/registry.test.ts +90 -0
- package/prover/registry.ts +82 -0
- package/schema.ts +17 -0
- package/setup-artifacts.sh +57 -0
- package/state/index.ts +2 -0
- package/state/merkle/hydrator.ts +69 -0
- package/state/merkle/index.ts +12 -0
- package/state/merkle/merkle-tree.test.ts +50 -0
- package/state/merkle/merkle-tree.ts +163 -0
- package/state/store/ciphertext-store.ts +28 -0
- package/state/store/index.ts +24 -0
- package/state/store/job-store.ts +162 -0
- package/state/store/jobs.ts +64 -0
- package/state/store/leaf-store.ts +39 -0
- package/state/store/note-store.ts +177 -0
- package/state/store/nullifier-store.ts +39 -0
- package/state/store/records.ts +61 -0
- package/state/store/root-store.ts +34 -0
- package/state/store/store.ts +25 -0
- package/state.test.ts +235 -0
- package/storage/index.ts +3 -0
- package/storage/indexeddb.test.ts +99 -0
- package/storage/indexeddb.ts +235 -0
- package/storage/memory.test.ts +59 -0
- package/storage/memory.ts +93 -0
- package/transactions/deposit.test.ts +160 -0
- package/transactions/deposit.ts +227 -0
- package/transactions/index.ts +20 -0
- package/transactions/note-sync.test.ts +155 -0
- package/transactions/note-sync.ts +452 -0
- package/transactions/reconcile.ts +73 -0
- package/transactions/transact.test.ts +451 -0
- package/transactions/transact.ts +811 -0
- package/transactions/types.ts +141 -0
- package/tsconfig.json +14 -0
- package/types/global.d.ts +15 -0
- package/types.ts +24 -0
- package/utils/async.ts +15 -0
- package/utils/bigint.ts +34 -0
- package/utils/crypto.test.ts +69 -0
- package/utils/crypto.ts +58 -0
- package/utils/json-codec.ts +38 -0
- package/utils/polling.ts +6 -0
- package/utils/signature.ts +16 -0
- package/utils/validators.test.ts +64 -0
- package/utils/validators.ts +86 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { FetchLike } from "./http.js";
|
|
2
|
+
import { createJsonHttpClient } from "./http.js";
|
|
3
|
+
import { isNotFoundError } from "../utils/async.js";
|
|
4
|
+
|
|
5
|
+
// The HTTP API returns snake_case fields; the `Raw` helpers mirror that
|
|
6
|
+
// payload exactly so we can normalize everything to camelCase in one place.
|
|
7
|
+
type CommitmentRecordRaw = {
|
|
8
|
+
leaf_index: number;
|
|
9
|
+
commitment: string;
|
|
10
|
+
ciphertext: [string, string, string];
|
|
11
|
+
root: string;
|
|
12
|
+
tx_hash: string;
|
|
13
|
+
inserted_at: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CommitmentRecord = {
|
|
17
|
+
index: number;
|
|
18
|
+
commitment: string;
|
|
19
|
+
ciphertext: [string, string, string];
|
|
20
|
+
root: string;
|
|
21
|
+
txHash: string;
|
|
22
|
+
insertedAt: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type NullifierRecordRaw = {
|
|
26
|
+
nullifier: string;
|
|
27
|
+
tx_hash: string;
|
|
28
|
+
spent_at: number;
|
|
29
|
+
chain_id: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type IndexerNullifierRecord = {
|
|
33
|
+
nullifier: string;
|
|
34
|
+
txHash: string;
|
|
35
|
+
spentAt: number;
|
|
36
|
+
chainId: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type CommitmentBatchResponseRaw = {
|
|
40
|
+
chain_id: number;
|
|
41
|
+
commitments: CommitmentRecordRaw[];
|
|
42
|
+
latest_root: string | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type CommitmentBatchResponse = {
|
|
46
|
+
chainId: number;
|
|
47
|
+
commitments: CommitmentRecord[];
|
|
48
|
+
latestRoot: string | null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type FetchCommitmentsParams = {
|
|
52
|
+
chainId: number;
|
|
53
|
+
start?: number;
|
|
54
|
+
limit?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type GetCommitmentParams = {
|
|
58
|
+
chainId: number;
|
|
59
|
+
commitment: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type GetNullifierParams = {
|
|
63
|
+
chainId: number;
|
|
64
|
+
nullifier: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type NullifierCountResponseRaw = {
|
|
68
|
+
chain_id: number;
|
|
69
|
+
count: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type IndexerClientDeps = {
|
|
73
|
+
fetch: FetchLike;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function createIndexerClient(baseUrl: string, deps: IndexerClientDeps) {
|
|
77
|
+
const http = createJsonHttpClient(baseUrl, deps);
|
|
78
|
+
|
|
79
|
+
// Converts the snake_case record from the REST API into the camelCase shape
|
|
80
|
+
// the rest of the SDK consumes.
|
|
81
|
+
const normalizeRecord = (record: CommitmentRecordRaw): CommitmentRecord => ({
|
|
82
|
+
index: record.leaf_index,
|
|
83
|
+
commitment: record.commitment,
|
|
84
|
+
ciphertext: [...record.ciphertext] as [string, string, string],
|
|
85
|
+
root: record.root,
|
|
86
|
+
txHash: record.tx_hash,
|
|
87
|
+
insertedAt: record.inserted_at,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const normalizeNullifierRecord = (
|
|
91
|
+
record: NullifierRecordRaw,
|
|
92
|
+
): IndexerNullifierRecord => ({
|
|
93
|
+
nullifier: record.nullifier,
|
|
94
|
+
txHash: record.tx_hash,
|
|
95
|
+
spentAt: record.spent_at,
|
|
96
|
+
chainId: record.chain_id,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
async fetchCommitmentBatch(
|
|
101
|
+
params: FetchCommitmentsParams,
|
|
102
|
+
): Promise<CommitmentBatchResponse> {
|
|
103
|
+
const raw = await http.request<CommitmentBatchResponseRaw>({
|
|
104
|
+
method: "GET",
|
|
105
|
+
path: `/chains/${params.chainId}/commitments`,
|
|
106
|
+
query: {
|
|
107
|
+
start: params.start,
|
|
108
|
+
limit: params.limit,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
chainId: raw.chain_id,
|
|
113
|
+
commitments: raw.commitments.map(normalizeRecord),
|
|
114
|
+
latestRoot: raw.latest_root,
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
async getCommitment(
|
|
118
|
+
params: GetCommitmentParams,
|
|
119
|
+
): Promise<CommitmentRecord> {
|
|
120
|
+
const raw = await http.request<CommitmentRecordRaw>({
|
|
121
|
+
method: "GET",
|
|
122
|
+
path: `/chains/${params.chainId}/commitments/${params.commitment}`,
|
|
123
|
+
});
|
|
124
|
+
return normalizeRecord(raw);
|
|
125
|
+
},
|
|
126
|
+
async getNullifier(
|
|
127
|
+
params: GetNullifierParams,
|
|
128
|
+
): Promise<IndexerNullifierRecord | null> {
|
|
129
|
+
try {
|
|
130
|
+
const raw = await http.request<NullifierRecordRaw>({
|
|
131
|
+
method: "GET",
|
|
132
|
+
path: `/chains/${params.chainId}/nullifiers/${params.nullifier}`,
|
|
133
|
+
});
|
|
134
|
+
return normalizeNullifierRecord(raw);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (isNotFoundError(err)) {
|
|
137
|
+
return null; // Nullifier not spent on-chain
|
|
138
|
+
}
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
async getNullifierCount(chainId: number): Promise<number> {
|
|
143
|
+
const raw = await http.request<NullifierCountResponseRaw>({
|
|
144
|
+
method: "GET",
|
|
145
|
+
path: `/chains/${chainId}/nullifiers/count`,
|
|
146
|
+
});
|
|
147
|
+
return raw.count;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
package/config.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const DEFAULT_RPC_URL = "http://localhost:3000";
|
|
2
|
+
|
|
3
|
+
function readEnv(key: string): string | undefined {
|
|
4
|
+
if (typeof process !== "undefined" && process?.env) {
|
|
5
|
+
return process.env[key];
|
|
6
|
+
}
|
|
7
|
+
// Support environments where process isn't defined but a shim sets globalThis.process
|
|
8
|
+
const globalProcess = (
|
|
9
|
+
globalThis as typeof globalThis & {
|
|
10
|
+
process?: { env?: Record<string, string | undefined> };
|
|
11
|
+
}
|
|
12
|
+
).process;
|
|
13
|
+
return globalProcess?.env?.[key];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readServiceUrl(envKey: string, fallback: string): string {
|
|
17
|
+
const raw = readEnv(envKey);
|
|
18
|
+
if (!raw) return fallback;
|
|
19
|
+
const trimmed = raw.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
throw new Error(`${envKey} must not be empty when set`);
|
|
22
|
+
}
|
|
23
|
+
return trimmed.replace(/\/+$/, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const serviceConfig = {
|
|
27
|
+
/**
|
|
28
|
+
* Base URL for the Broadcaster HTTP API (default: http://localhost:8081).
|
|
29
|
+
*/
|
|
30
|
+
broadcasterBaseUrl:
|
|
31
|
+
readServiceUrl("UNLINK_RPC_URL", DEFAULT_RPC_URL) + "/broadcaster",
|
|
32
|
+
/**
|
|
33
|
+
* Base URL for the Indexer HTTP API (default: http://localhost:8082).
|
|
34
|
+
*/
|
|
35
|
+
indexerBaseUrl:
|
|
36
|
+
readServiceUrl("UNLINK_RPC_URL", DEFAULT_RPC_URL) + "/indexer",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ServiceConfig = typeof serviceConfig;
|
package/core.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { migrate } from "./schema.js";
|
|
2
|
+
import type { Storage } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export type CoreDeps = {
|
|
5
|
+
storage: Storage;
|
|
6
|
+
rng: (n: number) => Uint8Array;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function initCore(deps: CoreDeps) {
|
|
10
|
+
if (!deps?.storage) throw new Error("storage dep required");
|
|
11
|
+
if (!deps?.rng) throw new Error("rng dep required");
|
|
12
|
+
await migrate(deps.storage);
|
|
13
|
+
return {
|
|
14
|
+
storage: deps.storage,
|
|
15
|
+
rng: deps.rng,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick smoke-test to guarantee the new dependency surface compiles and behaves as expected.
|
|
3
|
+
* This is a temporary helper that will be removed once zkAccount is migrated.
|
|
4
|
+
*/
|
|
5
|
+
export declare const railgunDependencySmokeTest: () => Promise<{
|
|
6
|
+
mnemonicRoundTrip: string;
|
|
7
|
+
packedPoint: Uint8Array<ArrayBufferLike>;
|
|
8
|
+
viewingKey: Uint8Array<ArrayBufferLike>;
|
|
9
|
+
address: `0zk1${string}`;
|
|
10
|
+
poseidonHash: bigint;
|
|
11
|
+
}>;
|
|
12
|
+
//# sourceMappingURL=railgun-imports-prototype.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"railgun-imports-prototype.d.ts","sourceRoot":"","sources":["../../account/railgun-imports-prototype.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,eAAO,MAAM,0BAA0B;;;;;;EAwBtC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { babyjub, poseidon } from '@railgun-community/circomlibjs';
|
|
2
|
+
import { getPublicKey } from '@noble/ed25519';
|
|
3
|
+
import { sha512 } from '@noble/hashes/sha512';
|
|
4
|
+
import { bech32m } from '@scure/base';
|
|
5
|
+
import { entropyToMnemonic, generateMnemonic, mnemonicToEntropy, mnemonicToSeedSync, validateMnemonic, } from '@scure/bip39';
|
|
6
|
+
import { wordlist } from '@scure/bip39/wordlists/english';
|
|
7
|
+
/**
|
|
8
|
+
* Quick smoke-test to guarantee the new dependency surface compiles and behaves as expected.
|
|
9
|
+
* This is a temporary helper that will be removed once zkAccount is migrated.
|
|
10
|
+
*/
|
|
11
|
+
export const railgunDependencySmokeTest = async () => {
|
|
12
|
+
const mnemonic = generateMnemonic(wordlist);
|
|
13
|
+
validateMnemonic(mnemonic, wordlist);
|
|
14
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
15
|
+
const entropy = mnemonicToEntropy(mnemonic, wordlist);
|
|
16
|
+
const mnemonicRoundTrip = entropyToMnemonic(entropy, wordlist);
|
|
17
|
+
const babyjubPoint = babyjub.Base8;
|
|
18
|
+
const packedPoint = babyjub.packPoint(babyjubPoint);
|
|
19
|
+
const viewingKey = await getPublicKey(seed.slice(0, 32));
|
|
20
|
+
const digest = sha512(seed);
|
|
21
|
+
const address = bech32m.encode('0zk', bech32m.toWords(digest.subarray(0, 32)), 127);
|
|
22
|
+
const poseidonHash = poseidon([BigInt(1), BigInt(2), BigInt(3)]);
|
|
23
|
+
return {
|
|
24
|
+
mnemonicRoundTrip,
|
|
25
|
+
packedPoint,
|
|
26
|
+
viewingKey,
|
|
27
|
+
address,
|
|
28
|
+
poseidonHash,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../../clients/indexer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../../clients/indexer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAe3C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AASF,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAQF,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAOF,KAAK,iBAAiB,GAAG;IACvB,KAAK,EAAE,SAAS,CAAC;CAClB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB;iCAyB9D,sBAAsB,GAC7B,OAAO,CAAC,uBAAuB,CAAC;0BAgBzB,mBAAmB,GAC1B,OAAO,CAAC,gBAAgB,CAAC;yBAQlB,kBAAkB,GACzB,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;+BAcR,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;EAQ5D"}
|
package/dist/clients/indexer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isNotFoundError } from "../utils/async.js";
|
|
2
1
|
import { createJsonHttpClient } from "./http.js";
|
|
2
|
+
import { isNotFoundError } from "../utils/async.js";
|
|
3
3
|
export function createIndexerClient(baseUrl, deps) {
|
|
4
4
|
const http = createJsonHttpClient(baseUrl, deps);
|
|
5
5
|
// Converts the snake_case record from the REST API into the camelCase shape
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type LeafLoader = (chainId: number, index: number) => Promise<{
|
|
2
|
+
commitment: string;
|
|
3
|
+
} | null>;
|
|
4
|
+
export type HydrateChainParams = {
|
|
5
|
+
chainId: number;
|
|
6
|
+
trees: {
|
|
7
|
+
reset(chainId: number): void;
|
|
8
|
+
addLeaf(chainId: number, value: bigint): {
|
|
9
|
+
index: number;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
loadLeaf: LeafLoader;
|
|
13
|
+
hydrated: Set<number>;
|
|
14
|
+
};
|
|
15
|
+
export declare function hydrateChain({ chainId, trees, loadLeaf, hydrated, }: HydrateChainParams): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=hydrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydrator.d.ts","sourceRoot":"","sources":["../../state/hydrator.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG,CACvB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,KACV,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAAC;AAE5C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE;QACL,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;KAC5D,CAAC;IACF,QAAQ,EAAE,UAAU,CAAC;IACrB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB,CAAC;AAEF,wBAAsB,YAAY,CAAC,EACjC,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE,kBAAkB,iBAcpB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parseHexToBigInt } from "../utils/bigint.js";
|
|
2
|
+
export async function hydrateChain({ chainId, trees, loadLeaf, hydrated, }) {
|
|
3
|
+
if (hydrated.has(chainId))
|
|
4
|
+
return;
|
|
5
|
+
trees.reset(chainId);
|
|
6
|
+
let idx = 0;
|
|
7
|
+
for (;;) {
|
|
8
|
+
const leaf = await loadLeaf(chainId, idx);
|
|
9
|
+
if (!leaf)
|
|
10
|
+
break;
|
|
11
|
+
const { index } = trees.addLeaf(chainId, parseHexToBigInt(leaf.commitment));
|
|
12
|
+
if (index !== idx) {
|
|
13
|
+
throw new Error("stored leaves are inconsistent with local tree");
|
|
14
|
+
}
|
|
15
|
+
idx += 1;
|
|
16
|
+
}
|
|
17
|
+
hydrated.add(chainId);
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Storage } from "../types.js";
|
|
2
|
+
import type { JobStatus, PendingJobKind, PendingJobRecord } from "./jobs.js";
|
|
3
|
+
export declare function createJobStore(storage: Storage): {
|
|
4
|
+
putPendingJob(job: PendingJobRecord): Promise<void>;
|
|
5
|
+
getPendingJob(relayId: string): Promise<PendingJobRecord | null>;
|
|
6
|
+
listPendingJobs(filter?: {
|
|
7
|
+
kind?: PendingJobKind;
|
|
8
|
+
statuses?: JobStatus[];
|
|
9
|
+
}): Promise<PendingJobRecord[]>;
|
|
10
|
+
deletePendingJob(relayId: string): Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=job-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-store.d.ts","sourceRoot":"","sources":["../../state/job-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAO3C,OAAO,KAAK,EACV,SAAS,EAET,cAAc,EACd,gBAAgB,EAGjB,MAAM,WAAW,CAAC;AA4GnB,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO;uBAElB,gBAAgB;2BASZ,MAAM;6BAKzB;QACN,IAAI,CAAC,EAAE,cAAc,CAAC;QACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;KACxB;8BAe6B,MAAM;EAIzC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { keys } from "../keys.js";
|
|
2
|
+
import { decodeJson, encodeJson, getJson } from "../utils/json-codec.js";
|
|
3
|
+
import { ensureAddress, ensureChainId, ensurePositiveInt, } from "../utils/validators.js";
|
|
4
|
+
import { DEFAULT_JOB_TIMEOUT_MS } from "./jobs.js";
|
|
5
|
+
const VALID_STATUSES = [
|
|
6
|
+
"pending",
|
|
7
|
+
"submitted",
|
|
8
|
+
"broadcasting",
|
|
9
|
+
"succeeded",
|
|
10
|
+
"failed",
|
|
11
|
+
"dead",
|
|
12
|
+
];
|
|
13
|
+
function assertStatus(status) {
|
|
14
|
+
if (!VALID_STATUSES.includes(status)) {
|
|
15
|
+
throw new Error(`invalid job status: ${status}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function assertKind(kind) {
|
|
19
|
+
if (kind !== "deposit" && kind !== "transact") {
|
|
20
|
+
throw new Error(`invalid job kind: ${kind}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function normalizeTimestamps(job) {
|
|
24
|
+
const createdAt = typeof job.createdAt === "number" && Number.isFinite(job.createdAt)
|
|
25
|
+
? job.createdAt
|
|
26
|
+
: Date.now();
|
|
27
|
+
const timeoutMs = typeof job.timeoutMs === "number" && job.timeoutMs > 0
|
|
28
|
+
? job.timeoutMs
|
|
29
|
+
: DEFAULT_JOB_TIMEOUT_MS;
|
|
30
|
+
return {
|
|
31
|
+
...job,
|
|
32
|
+
createdAt,
|
|
33
|
+
timeoutMs,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function validateTransactContext(context) {
|
|
37
|
+
ensurePositiveInt("context index", context.index);
|
|
38
|
+
if (!context.nullifier) {
|
|
39
|
+
throw new Error("context nullifier is required");
|
|
40
|
+
}
|
|
41
|
+
const { witness } = context;
|
|
42
|
+
if (!witness) {
|
|
43
|
+
throw new Error("context witness is required");
|
|
44
|
+
}
|
|
45
|
+
ensurePositiveInt("witness leafIndex", witness.leafIndex);
|
|
46
|
+
if (!Array.isArray(witness.pathElements) ||
|
|
47
|
+
!Array.isArray(witness.pathIndices)) {
|
|
48
|
+
throw new Error("witness pathElements and pathIndices are required");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function validateTransactJob(job) {
|
|
52
|
+
ensureAddress("pool address", job.poolAddress);
|
|
53
|
+
if (!job.calldata) {
|
|
54
|
+
throw new Error("calldata is required for transact job");
|
|
55
|
+
}
|
|
56
|
+
job.contexts.forEach(validateTransactContext);
|
|
57
|
+
job.predictedOutputs.forEach((output) => {
|
|
58
|
+
if (!output.hex) {
|
|
59
|
+
throw new Error("predicted output hex is required");
|
|
60
|
+
}
|
|
61
|
+
ensurePositiveInt("predicted output index", output.index);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function validateDepositJob(job) {
|
|
65
|
+
if (!job.predictedCommitment?.hex) {
|
|
66
|
+
throw new Error("predicted commitment hex is required");
|
|
67
|
+
}
|
|
68
|
+
if (job.predictedCommitment.index !== undefined) {
|
|
69
|
+
ensurePositiveInt("predicted commitment index", job.predictedCommitment.index);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function validateJob(job) {
|
|
73
|
+
if (!job.relayId) {
|
|
74
|
+
throw new Error("relayId is required");
|
|
75
|
+
}
|
|
76
|
+
ensureChainId(job.chainId);
|
|
77
|
+
assertStatus(job.status);
|
|
78
|
+
assertKind(job.kind);
|
|
79
|
+
ensurePositiveInt("job createdAt", job.createdAt);
|
|
80
|
+
ensurePositiveInt("job timeoutMs", job.timeoutMs);
|
|
81
|
+
if (job.lastCheckedAt !== undefined) {
|
|
82
|
+
ensurePositiveInt("job lastCheckedAt", job.lastCheckedAt);
|
|
83
|
+
}
|
|
84
|
+
if (job.kind === "deposit") {
|
|
85
|
+
validateDepositJob(job);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
validateTransactJob(job);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function buildJobKey(relayId) {
|
|
92
|
+
return keys.job(relayId);
|
|
93
|
+
}
|
|
94
|
+
export function createJobStore(storage) {
|
|
95
|
+
return {
|
|
96
|
+
async putPendingJob(job) {
|
|
97
|
+
const normalized = normalizeTimestamps(job);
|
|
98
|
+
validateJob(normalized);
|
|
99
|
+
await storage.put(buildJobKey(normalized.relayId), encodeJson(normalized));
|
|
100
|
+
},
|
|
101
|
+
async getPendingJob(relayId) {
|
|
102
|
+
return getJson(storage, buildJobKey(relayId));
|
|
103
|
+
},
|
|
104
|
+
async listPendingJobs(filter = {}) {
|
|
105
|
+
const entries = await storage.iter({ prefix: "jobs:" });
|
|
106
|
+
const statuses = filter.statuses ?? VALID_STATUSES;
|
|
107
|
+
const filtered = entries
|
|
108
|
+
.map(({ value }) => decodeJson(value))
|
|
109
|
+
.filter((job) => (filter.kind === undefined || job.kind === filter.kind) &&
|
|
110
|
+
statuses.includes(job.status));
|
|
111
|
+
filtered.sort((a, b) => a.createdAt - b.createdAt);
|
|
112
|
+
return filtered;
|
|
113
|
+
},
|
|
114
|
+
async deletePendingJob(relayId) {
|
|
115
|
+
await storage.delete(buildJobKey(relayId));
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type JobStatus = "pending" | "submitted" | "broadcasting" | "succeeded" | "failed" | "dead";
|
|
2
|
+
export type PendingJobKind = "deposit" | "transact";
|
|
3
|
+
export type PendingJobBase = {
|
|
4
|
+
relayId: string;
|
|
5
|
+
kind: PendingJobKind;
|
|
6
|
+
chainId: number;
|
|
7
|
+
status: JobStatus;
|
|
8
|
+
broadcasterRelayId?: string | null;
|
|
9
|
+
txHash?: string | null;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
lastCheckedAt?: number;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
error?: string | null;
|
|
14
|
+
};
|
|
15
|
+
export type PendingDepositJob = PendingJobBase & {
|
|
16
|
+
kind: "deposit";
|
|
17
|
+
predictedCommitment: {
|
|
18
|
+
hex: string;
|
|
19
|
+
index?: number;
|
|
20
|
+
root?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type PendingTransactContext = {
|
|
24
|
+
index: number;
|
|
25
|
+
nullifier: string;
|
|
26
|
+
witness: {
|
|
27
|
+
root: string;
|
|
28
|
+
leaf: string;
|
|
29
|
+
pathElements: string[][];
|
|
30
|
+
pathIndices: number[];
|
|
31
|
+
leafIndex: number;
|
|
32
|
+
};
|
|
33
|
+
root: string;
|
|
34
|
+
};
|
|
35
|
+
export type PendingTransactOutput = {
|
|
36
|
+
hex: string;
|
|
37
|
+
index: number;
|
|
38
|
+
root?: string;
|
|
39
|
+
};
|
|
40
|
+
export type PendingTransactJob = PendingJobBase & {
|
|
41
|
+
kind: "transact";
|
|
42
|
+
poolAddress: string;
|
|
43
|
+
calldata: string;
|
|
44
|
+
contexts: PendingTransactContext[];
|
|
45
|
+
predictedOutputs: PendingTransactOutput[];
|
|
46
|
+
expectedRoot?: string;
|
|
47
|
+
};
|
|
48
|
+
export type PendingJobRecord = PendingDepositJob | PendingTransactJob;
|
|
49
|
+
export declare const DEFAULT_JOB_TIMEOUT_MS: number;
|
|
50
|
+
//# sourceMappingURL=jobs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../state/jobs.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,WAAW,GACX,cAAc,GACd,WAAW,GACX,QAAQ,GACR,MAAM,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,UAAU,CAAC;AAEpD,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG;IAC/C,IAAI,EAAE,SAAS,CAAC;IAChB,mBAAmB,EAAE;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAElB,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC;QACzB,WAAW,EAAE,MAAM,EAAE,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,cAAc,GAAG;IAChD,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,sBAAsB,EAAE,CAAC;IACnC,gBAAgB,EAAE,qBAAqB,EAAE,CAAC;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAEtE,eAAO,MAAM,sBAAsB,QAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_JOB_TIMEOUT_MS = 5 * 60 * 1000;
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Storage } from './types.js';
|
|
2
|
+
export declare function ensurePositiveInt(label: string, value: number): void;
|
|
3
|
+
export declare function ensureChainId(chainId: number): void;
|
|
4
|
+
/**
|
|
5
|
+
* Locally cached metadata for a shielded note we consider owned.
|
|
6
|
+
*/
|
|
7
|
+
export type NoteRecord = {
|
|
8
|
+
chainId: number;
|
|
9
|
+
/** Leaf index within the on-chain tree (zero-based, monotonic). */
|
|
10
|
+
index: number;
|
|
11
|
+
/** Canonical asset identifier, typically the ERC-20 address lowercased. */
|
|
12
|
+
token: string;
|
|
13
|
+
/** Amount expressed as an unsigned BigInt serialized to a base-10 string. */
|
|
14
|
+
value: string;
|
|
15
|
+
/** Commitment stored on-chain (Poseidon(note)). */
|
|
16
|
+
commitment: string;
|
|
17
|
+
/** Nullifier public key derived from the account's master keys. */
|
|
18
|
+
npk: string;
|
|
19
|
+
/**
|
|
20
|
+
* Master public key for the account (Poseidon(spendingPk.public, nullifyingKey)).
|
|
21
|
+
* Acts as the stable account identifier when grouping notes locally.
|
|
22
|
+
*/
|
|
23
|
+
mpk: string;
|
|
24
|
+
/** Randomizer used when committing the note. */
|
|
25
|
+
random: string;
|
|
26
|
+
/** Nullifier that will be revealed when the note is spent. */
|
|
27
|
+
nullifier: string;
|
|
28
|
+
/** Optional timestamp when the note was marked as spent. */
|
|
29
|
+
spentAt?: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Details about a nullifier observed on-chain, whether or not we own the note.
|
|
33
|
+
*/
|
|
34
|
+
export type NullifierRecord = {
|
|
35
|
+
chainId: number;
|
|
36
|
+
/** Nullifier value (expected to match NoteRecord.nullifier). */
|
|
37
|
+
nullifier: string;
|
|
38
|
+
/** Leaf index of the corresponding note when known. */
|
|
39
|
+
noteIndex?: number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Public information about a leaf commitment inserted into the pool's tree.
|
|
43
|
+
*/
|
|
44
|
+
export type LeafRecord = {
|
|
45
|
+
chainId: number;
|
|
46
|
+
/** Leaf index inside the tree (zero-based). */
|
|
47
|
+
index: number;
|
|
48
|
+
/** Commitment value stored in the tree. */
|
|
49
|
+
commitment: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Historical merkle root snapshot used for proof construction.
|
|
53
|
+
*/
|
|
54
|
+
export type RootRecord = {
|
|
55
|
+
chainId: number;
|
|
56
|
+
/** Sequence number emitted by the pool contract; increments every time the root updates. */
|
|
57
|
+
sequence: number;
|
|
58
|
+
/** Merkle root value committed on-chain. */
|
|
59
|
+
root: string;
|
|
60
|
+
};
|
|
61
|
+
export declare function createStateStore(storage: Storage): {
|
|
62
|
+
putNote(note: NoteRecord): Promise<void>;
|
|
63
|
+
getNote(chainId: number, index: number): Promise<NoteRecord | null>;
|
|
64
|
+
listNotes(options?: {
|
|
65
|
+
chainId?: number;
|
|
66
|
+
mpk?: string;
|
|
67
|
+
includeSpent?: boolean;
|
|
68
|
+
}): Promise<NoteRecord[]>;
|
|
69
|
+
markNoteSpent(chainId: number, index: number, spentAt?: number): Promise<NoteRecord>;
|
|
70
|
+
markNoteUnspent(chainId: number, index: number): Promise<NoteRecord>;
|
|
71
|
+
getShieldedBalance(mpk: string, options?: {
|
|
72
|
+
chainId?: number;
|
|
73
|
+
}): Promise<Record<string, bigint>>;
|
|
74
|
+
putNullifier(nullifier: NullifierRecord): Promise<void>;
|
|
75
|
+
getNullifier(chainId: number, value: string): Promise<NullifierRecord | null>;
|
|
76
|
+
putLeaf(leaf: LeafRecord): Promise<void>;
|
|
77
|
+
getLeaf(chainId: number, index: number): Promise<LeafRecord | null>;
|
|
78
|
+
putRoot(root: RootRecord): Promise<void>;
|
|
79
|
+
getRoot(chainId: number, sequence: number): Promise<RootRecord | null>;
|
|
80
|
+
putCiphertext(chainId: number, index: number, payload: Uint8Array): Promise<void>;
|
|
81
|
+
getCiphertext(chainId: number, index: number): Promise<Uint8Array<ArrayBuffer> | null>;
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAW,OAAO,EAAE,MAAM,YAAY,CAAC;AASnD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAI7D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,QAI5C;AAkBD;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAElB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;CAIpB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;CAIpB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;CAGd,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO;kBAEzB,UAAU;qBAsCP,MAAM,SAAS,MAAM;wBAOjC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE;2BAuB1D,MAAM,SACR,MAAM;6BAkBgB,MAAM,SAAS,MAAM;4BAiB7C,MAAM,YACF;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;4BAgBD,eAAe;0BASjB,MAAM,SAAS,MAAM;kBAK7B,UAAU;qBAMP,MAAM,SAAS,MAAM;kBAMxB,UAAU;qBAMP,MAAM,YAAY,MAAM;2BAMlB,MAAM,SAAS,MAAM,WAAW,UAAU;2BAQ1C,MAAM,SAAS,MAAM;EASrD"}
|