@provex/utils 1.9.2 → 1.10.0-rc.20260820232714.83636f8
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/dist/chain.d.ts +13 -0
- package/dist/chain.js +13 -0
- package/dist/contracts.config.test.d.ts +18 -0
- package/dist/contracts.config.test.js +220 -0
- package/dist/contracts.d.ts +169 -29
- package/dist/contracts.js +188 -31
- package/dist/contracts.test.js +134 -7
- package/dist/deposit.d.ts +50 -0
- package/dist/deposit.js +45 -0
- package/dist/events.d.ts +2 -2
- package/dist/events.js +20 -0
- package/dist/payment.currency.test.d.ts +11 -0
- package/dist/payment.currency.test.js +134 -0
- package/dist/payment.d.ts +149 -23
- package/dist/payment.js +170 -29
- package/dist/payment.rails.test.d.ts +25 -0
- package/dist/payment.rails.test.js +213 -0
- package/dist/payment.test.js +73 -51
- package/dist/rateInput.d.ts +163 -0
- package/dist/rateInput.js +173 -0
- package/dist/rateInput.test.d.ts +1 -0
- package/dist/rateInput.test.js +318 -0
- package/dist/tokens.d.ts +15068 -15037
- package/dist/tokens.js +37 -8
- package/dist/tokens.test.js +11 -0
- package/dist/transports.d.ts +86 -8
- package/dist/transports.js +126 -10
- package/dist/transports.test.js +131 -5
- package/dist/units.d.ts +9 -0
- package/dist/units.js +9 -0
- package/package.json +5 -1
package/dist/chain.d.ts
CHANGED
|
@@ -380,6 +380,19 @@ export declare const chainIds: (369 | 8453)[];
|
|
|
380
380
|
* ```
|
|
381
381
|
*/
|
|
382
382
|
export type ChainId = (typeof chainIds)[number];
|
|
383
|
+
/**
|
|
384
|
+
* Narrow an arbitrary number to a supported chain id.
|
|
385
|
+
*
|
|
386
|
+
* Chain ids reach the server as untyped JSON — a request body, a query string,
|
|
387
|
+
* a `Number(...)` of either. Casting those straight to {@link ChainId} tells
|
|
388
|
+
* the compiler a lie, and the lookup that follows silently returns nothing for
|
|
389
|
+
* a chain we never supported. Narrow first so an unsupported chain is a branch
|
|
390
|
+
* the caller has to handle rather than an undefined it discovers later.
|
|
391
|
+
*
|
|
392
|
+
* @param value - Any number, typically parsed from a request
|
|
393
|
+
* @returns True when the value is one of the supported chain ids
|
|
394
|
+
*/
|
|
395
|
+
export declare const isChainId: (value: number) => value is ChainId;
|
|
383
396
|
/**
|
|
384
397
|
* Human-readable chain names for URL params and display.
|
|
385
398
|
* Maps chain IDs to lowercase slug-style names for URL-safe usage.
|
package/dist/chain.js
CHANGED
|
@@ -22,6 +22,19 @@ export const chains = [pulsechain, base];
|
|
|
22
22
|
* Currently: [369 (Pulsechain), 8453 (Base)]
|
|
23
23
|
*/
|
|
24
24
|
export const chainIds = chains.map((chain) => chain.id);
|
|
25
|
+
/**
|
|
26
|
+
* Narrow an arbitrary number to a supported chain id.
|
|
27
|
+
*
|
|
28
|
+
* Chain ids reach the server as untyped JSON — a request body, a query string,
|
|
29
|
+
* a `Number(...)` of either. Casting those straight to {@link ChainId} tells
|
|
30
|
+
* the compiler a lie, and the lookup that follows silently returns nothing for
|
|
31
|
+
* a chain we never supported. Narrow first so an unsupported chain is a branch
|
|
32
|
+
* the caller has to handle rather than an undefined it discovers later.
|
|
33
|
+
*
|
|
34
|
+
* @param value - Any number, typically parsed from a request
|
|
35
|
+
* @returns True when the value is one of the supported chain ids
|
|
36
|
+
*/
|
|
37
|
+
export const isChainId = (value) => chainIds.includes(value);
|
|
25
38
|
/**
|
|
26
39
|
* Human-readable chain names for URL params and display.
|
|
27
40
|
* Maps chain IDs to lowercase slug-style names for URL-safe usage.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configuration invariants for the contract registry.
|
|
3
|
+
*
|
|
4
|
+
* `contracts.test.ts` tests what the accessor functions *do*. This file tests
|
|
5
|
+
* whether the data they read is *internally consistent* — the class of mistake
|
|
6
|
+
* that has no wrong-looking code anywhere, only a wrong address or a missing
|
|
7
|
+
* key, and that surfaces in production as an empty orderbook or a reverted
|
|
8
|
+
* submission rather than as a thrown error.
|
|
9
|
+
*
|
|
10
|
+
* Every test here sweeps the whole registry rather than spot-checking a chain,
|
|
11
|
+
* so registering a new generation or a new chain is covered the moment it lands
|
|
12
|
+
* in `contracts` — nobody has to remember to extend a list.
|
|
13
|
+
*
|
|
14
|
+
* These invariants are what makes the v4 flip safe: registering
|
|
15
|
+
* `contracts.v4[chainId]` is supposed to be the *only* step, and each assertion
|
|
16
|
+
* below is one of the things that silently has to be true for that to hold.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configuration invariants for the contract registry.
|
|
3
|
+
*
|
|
4
|
+
* `contracts.test.ts` tests what the accessor functions *do*. This file tests
|
|
5
|
+
* whether the data they read is *internally consistent* — the class of mistake
|
|
6
|
+
* that has no wrong-looking code anywhere, only a wrong address or a missing
|
|
7
|
+
* key, and that surfaces in production as an empty orderbook or a reverted
|
|
8
|
+
* submission rather than as a thrown error.
|
|
9
|
+
*
|
|
10
|
+
* Every test here sweeps the whole registry rather than spot-checking a chain,
|
|
11
|
+
* so registering a new generation or a new chain is covered the moment it lands
|
|
12
|
+
* in `contracts` — nobody has to remember to extend a list.
|
|
13
|
+
*
|
|
14
|
+
* These invariants are what makes the v4 flip safe: registering
|
|
15
|
+
* `contracts.v4[chainId]` is supposed to be the *only* step, and each assertion
|
|
16
|
+
* below is one of the things that silently has to be true for that to hold.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it } from 'node:test';
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { getAddress, isAddress } from 'viem';
|
|
21
|
+
import { chainIds } from './chain.js';
|
|
22
|
+
import { contracts, versionKeys, getContracts, getLatestContract, getLatestConfiguredVersion, getPaymentAttestationVerifier, getAllEscrowContracts, getEscrowLocation, detectVersionFromEscrow, getConfiguredOrchestratorForEscrow, V3_PULSECHAIN_DEPLOYMENT_REFERENCE, V4_PULSECHAIN_DEPLOYMENT_REFERENCE, reclaimSigners, } from './contracts.js';
|
|
23
|
+
/** Every (version, chain) pair that has a contract set registered. */
|
|
24
|
+
const registeredSets = () => versionKeys.flatMap((version) => chainIds.flatMap((chainId) => {
|
|
25
|
+
const set = getContracts(version, chainId);
|
|
26
|
+
return set ? [{ version, chainId, set: set }] : [];
|
|
27
|
+
}));
|
|
28
|
+
/** Flatten a contract set to every address it holds, address-array or scalar. */
|
|
29
|
+
const addressesIn = (set) => Object.entries(set).flatMap(([key, value]) => {
|
|
30
|
+
if (typeof value === 'string')
|
|
31
|
+
return [{ key, address: value }];
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return value.map((address) => ({ key, address: String(address) }));
|
|
34
|
+
return [];
|
|
35
|
+
});
|
|
36
|
+
/** Read an address-array field off a contract set whose exact shape varies by generation. */
|
|
37
|
+
const addressArray = (set, key) => set?.[key] ?? [];
|
|
38
|
+
describe('contract registry configuration', () => {
|
|
39
|
+
describe('address hygiene', () => {
|
|
40
|
+
it('stores every address in EIP-55 checksummed form', () => {
|
|
41
|
+
// The registry runs each literal through viem's `getAddress`, so a
|
|
42
|
+
// checksum failure here means an address bypassed that call — and a
|
|
43
|
+
// non-checksummed address breaks the case-sensitive `===` comparisons
|
|
44
|
+
// that several call sites still do against these constants.
|
|
45
|
+
for (const { version, chainId, set } of registeredSets()) {
|
|
46
|
+
for (const { key, address } of addressesIn(set)) {
|
|
47
|
+
assert.strictEqual(address, getAddress(address), `${version}[${chainId}].${key} is not checksummed: ${address}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
it('holds only syntactically valid addresses, reference blocks included', () => {
|
|
52
|
+
// The `*_DEPLOYMENT_REFERENCE` objects are plain string literals — they
|
|
53
|
+
// never pass through `getAddress`, so nothing but this test would catch a
|
|
54
|
+
// truncated or mistyped ops address before someone pastes it into a
|
|
55
|
+
// `cast send`.
|
|
56
|
+
const referenced = [
|
|
57
|
+
...Object.entries(V3_PULSECHAIN_DEPLOYMENT_REFERENCE),
|
|
58
|
+
...Object.entries(V4_PULSECHAIN_DEPLOYMENT_REFERENCE),
|
|
59
|
+
...Object.entries(reclaimSigners),
|
|
60
|
+
];
|
|
61
|
+
for (const [key, address] of referenced) {
|
|
62
|
+
assert.ok(isAddress(address, { strict: false }), `${key} is not an address: ${address}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('escrow identity', () => {
|
|
67
|
+
it('never repeats an escrow address across generations or chains', () => {
|
|
68
|
+
// `getEscrowLocation` is a single address-keyed map, so a duplicate does
|
|
69
|
+
// not error — one of the two entries just wins and every deposit in the
|
|
70
|
+
// other is attributed to the wrong generation or the wrong chain.
|
|
71
|
+
const seen = new Map();
|
|
72
|
+
for (const { version, chainId, escrow } of getAllEscrowContracts()) {
|
|
73
|
+
const key = escrow.toLowerCase();
|
|
74
|
+
const owner = `${version}[${chainId}]`;
|
|
75
|
+
const previous = seen.get(key);
|
|
76
|
+
assert.strictEqual(previous, undefined, `${escrow} is claimed by both ${previous} and ${owner}`);
|
|
77
|
+
seen.set(key, owner);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
it('resolves every configured escrow back to its own version and chain', () => {
|
|
81
|
+
for (const location of getAllEscrowContracts()) {
|
|
82
|
+
assert.deepStrictEqual(getEscrowLocation(location.escrow), location, `${location.escrow} did not round-trip through getEscrowLocation`);
|
|
83
|
+
assert.strictEqual(detectVersionFromEscrow(location.escrow), location.version);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it('resolves escrows case-insensitively — indexer rows are lowercased', () => {
|
|
87
|
+
for (const location of getAllEscrowContracts()) {
|
|
88
|
+
assert.strictEqual(detectVersionFromEscrow(location.escrow.toLowerCase()), location.version);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('generation completeness', () => {
|
|
93
|
+
it('gives every registered v4 chain a full V4Contracts shape', () => {
|
|
94
|
+
// A v4 set missing `orchestrator` is the dangerous partial: deposit
|
|
95
|
+
// creation still flips to v4 (that only needs `escrow`), but
|
|
96
|
+
// `getConfiguredOrchestratorForEscrow` returns null and every intent read
|
|
97
|
+
// falls back to `escrow.orchestrator()`, which EscrowV2 does not expose.
|
|
98
|
+
for (const [chainIdStr, set] of Object.entries(contracts.v4)) {
|
|
99
|
+
const v4 = set;
|
|
100
|
+
const where = `v4[${chainIdStr}]`;
|
|
101
|
+
assert.ok(v4.usdc, `${where} has no usdc`);
|
|
102
|
+
assert.ok(v4.gatingService, `${where} has no gatingService`);
|
|
103
|
+
assert.ok(v4.escrow?.length, `${where} has no escrow`);
|
|
104
|
+
assert.ok(v4.unifiedPaymentVerifier?.length, `${where} has no unifiedPaymentVerifier`);
|
|
105
|
+
assert.ok(v4.orchestrator?.length, `${where} has no orchestrator`);
|
|
106
|
+
assert.ok(v4.orchestratorRegistry, `${where} has no orchestratorRegistry`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
it('answers the orchestrator from config for v4 and only for v4', () => {
|
|
110
|
+
for (const { version, escrow } of getAllEscrowContracts()) {
|
|
111
|
+
const orchestrator = getConfiguredOrchestratorForEscrow(escrow);
|
|
112
|
+
if (version === 'v4') {
|
|
113
|
+
assert.ok(orchestrator, `v4 escrow ${escrow} has no configured orchestrator`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// v2/v3 escrows expose `orchestrator()`; returning an address here
|
|
117
|
+
// would shadow the on-chain read with a stale constant.
|
|
118
|
+
assert.strictEqual(orchestrator, null, `${version} escrow ${escrow} should defer to the chain`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
it('does not reuse a v3 address anywhere in a v4 set', () => {
|
|
122
|
+
// The copy-paste failure when standing up a new generation. Reusing the
|
|
123
|
+
// v3 escrow means v4 deposits land in the v3 contract; reusing the v3
|
|
124
|
+
// verifier means attestations get signed against a retired domain.
|
|
125
|
+
for (const chainId of chainIds) {
|
|
126
|
+
const v3 = getContracts('v3', chainId);
|
|
127
|
+
const v4 = getContracts('v4', chainId);
|
|
128
|
+
if (!v3 || !v4)
|
|
129
|
+
continue;
|
|
130
|
+
for (const key of ['escrow', 'unifiedPaymentVerifier']) {
|
|
131
|
+
const v3Addresses = new Set(addressArray(v3, key).map((a) => a.toLowerCase()));
|
|
132
|
+
for (const address of addressArray(v4, key)) {
|
|
133
|
+
assert.ok(!v3Addresses.has(address.toLowerCase()), `v4[${chainId}].${key} reuses the v3 address ${address}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('version routing', () => {
|
|
140
|
+
it('routes each chain to a version that actually has an escrow', () => {
|
|
141
|
+
for (const chainId of chainIds) {
|
|
142
|
+
const version = getLatestConfiguredVersion(chainId);
|
|
143
|
+
assert.ok(getContracts(version, chainId)?.escrow?.length, `getLatestConfiguredVersion(${chainId}) chose ${version}, which has no escrow`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
it('routes to the newest registered generation, not merely a configured one', () => {
|
|
147
|
+
// Derives the expectation from the declaration order of `versions`
|
|
148
|
+
// instead of restating the precedence list. Adding `v5` to `contracts`
|
|
149
|
+
// but forgetting it in VERSION_PRECEDENCE fails here — the flip would
|
|
150
|
+
// otherwise stay silently on v4 with no code looking wrong.
|
|
151
|
+
for (const chainId of chainIds) {
|
|
152
|
+
const newest = [...versionKeys]
|
|
153
|
+
.reverse()
|
|
154
|
+
.find((version) => getContracts(version, chainId)?.escrow?.length);
|
|
155
|
+
assert.strictEqual(getLatestConfiguredVersion(chainId), newest, `chain ${chainId} should route new deposits to ${newest}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
it('keeps `versions` and the `contracts` map in step', () => {
|
|
159
|
+
assert.deepStrictEqual(Object.keys(contracts).sort(), [...versionKeys].sort(), 'a generation exists in one of `contracts` / `versions` but not the other');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('cross-generation coupling', () => {
|
|
163
|
+
it('names the current verifier for the attestation domain on every chain', () => {
|
|
164
|
+
// `UnifiedPaymentVerifier` sets `verifyingContract = address(this)`, so
|
|
165
|
+
// this address is load-bearing for the on-chain signature check.
|
|
166
|
+
for (const chainId of chainIds) {
|
|
167
|
+
const verifier = getPaymentAttestationVerifier(chainId);
|
|
168
|
+
assert.ok(verifier, `chain ${chainId} has no payment attestation verifier`);
|
|
169
|
+
assert.strictEqual(verifier, getLatestContract(chainId, 'unifiedPaymentVerifier', getLatestConfiguredVersion(chainId)));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
it('never leaves the attestation domain on a superseded verifier', () => {
|
|
173
|
+
for (const chainId of chainIds) {
|
|
174
|
+
const current = getPaymentAttestationVerifier(chainId);
|
|
175
|
+
const latest = getLatestConfiguredVersion(chainId);
|
|
176
|
+
for (const version of versionKeys) {
|
|
177
|
+
if (version === latest || version === 'v2')
|
|
178
|
+
continue;
|
|
179
|
+
const superseded = getLatestContract(chainId, 'unifiedPaymentVerifier', version);
|
|
180
|
+
if (!superseded)
|
|
181
|
+
continue;
|
|
182
|
+
assert.notStrictEqual(current, superseded, `chain ${chainId} still signs attestations against the ${version} verifier`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
it('settles every generation on one chain in the same token', () => {
|
|
187
|
+
// Volume, reputation tiers, and the taker ladder all sum across
|
|
188
|
+
// generations in raw base units. If v3 and v4 settled in different
|
|
189
|
+
// tokens those sums would silently mix currencies and decimals.
|
|
190
|
+
for (const chainId of chainIds) {
|
|
191
|
+
const tokens = new Set(versionKeys
|
|
192
|
+
.map((version) => getContracts(version, chainId)?.usdc)
|
|
193
|
+
.filter((token) => !!token)
|
|
194
|
+
.map((token) => token.toLowerCase()));
|
|
195
|
+
assert.strictEqual(tokens.size, 1, `chain ${chainId} settles generations in ${tokens.size} tokens`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
it('shares one gating service across orchestrator generations on a chain', () => {
|
|
199
|
+
// Load-bearing, not cosmetic: several call sites still resolve the
|
|
200
|
+
// gating service without a version and therefore get the v3 entry
|
|
201
|
+
// (`getDefaultVersion` returns 'v3') — `AddressesMenu.tsx`, and
|
|
202
|
+
// `ProveXClient.ts` which pins 'v3' outright. Those are correct only
|
|
203
|
+
// while every orchestrator generation on a chain names the same signer.
|
|
204
|
+
// If a future generation ever needs its own, those call sites must be
|
|
205
|
+
// fixed in the same change that breaks this test.
|
|
206
|
+
//
|
|
207
|
+
// v2 is excluded deliberately: Base v2 does name a different signer, but
|
|
208
|
+
// nothing routes there (`getDefaultVersion` returns 'v3' and v2 is
|
|
209
|
+
// historical-read-only), so that divergence is inert.
|
|
210
|
+
for (const chainId of chainIds) {
|
|
211
|
+
const signers = new Set(versionKeys
|
|
212
|
+
.filter((version) => version !== 'v2')
|
|
213
|
+
.map((version) => getLatestContract(chainId, 'gatingService', version))
|
|
214
|
+
.filter((signer) => !!signer)
|
|
215
|
+
.map((signer) => signer.toLowerCase()));
|
|
216
|
+
assert.strictEqual(signers.size, 1, `chain ${chainId} has ${signers.size} gating services; version-less call sites will pick the wrong one`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
package/dist/contracts.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @see {@link abis} for contract ABIs
|
|
24
24
|
*/
|
|
25
25
|
import { type Hex } from 'viem';
|
|
26
|
-
import type
|
|
26
|
+
import { type ChainId } from './chain.js';
|
|
27
27
|
/**
|
|
28
28
|
* Contract type definitions for the app.
|
|
29
29
|
*/
|
|
@@ -85,29 +85,22 @@ export declare const v3BaseContracts: V3Contracts;
|
|
|
85
85
|
*/
|
|
86
86
|
export declare const v4BaseContracts: V4Contracts;
|
|
87
87
|
/**
|
|
88
|
-
* V4 contracts on PulseChain (Chain ID 369) —
|
|
89
|
-
*
|
|
90
|
-
* EscrowV2 + OrchestratorV2
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* `
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* export const v4PulsechainContracts = {
|
|
103
|
-
* usdc: getAddress('0x15D38573d2feeb82e7ad5187aB8c1D52810B1f07'), // same PLSD/USDC as v3
|
|
104
|
-
* gatingService: getAddress('0x…'),
|
|
105
|
-
* escrow: [getAddress('0x…')], // EscrowV2
|
|
106
|
-
* unifiedPaymentVerifier: [getAddress('0x…')], // UnifiedPaymentVerifierV2
|
|
107
|
-
* orchestrator: [getAddress('0x…')], // OrchestratorV2
|
|
108
|
-
* orchestratorRegistry: getAddress('0x…'),
|
|
109
|
-
* } as V4Contracts
|
|
88
|
+
* V4 contracts on PulseChain (Chain ID 369) — LIVE as of 2026-07-24.
|
|
89
|
+
*
|
|
90
|
+
* EscrowV2 + OrchestratorV2 landed on PulseChain mainnet at blocks
|
|
91
|
+
* 27112395–27112403 (deployer 0x948eB6D3A08BEb29Dc1D08d09753862573A94122).
|
|
92
|
+
* Bytecode verified on chain 369. Addresses taken from
|
|
93
|
+
* `zkp2p-contracts/deployments/pulsechain/{EscrowV2,OrchestratorV2,
|
|
94
|
+
* OrchestratorRegistry,UnifiedPaymentVerifierV2}.json`.
|
|
95
|
+
*
|
|
96
|
+
* Registered in the `contracts.v4` map below, so `getLatestConfiguredVersion(369)`
|
|
97
|
+
* now routes new PulseChain deposits to v4. This must ship alongside the indexer
|
|
98
|
+
* wiring in `packages/indexer/ponder.config.ts` (PulseChain v4 streams +
|
|
99
|
+
* `startBlocks.pulsechain.v4Escrow` = 27112398): deploy the indexer first so a
|
|
100
|
+
* fresh reindex covers the EscrowV2 events before the UI starts writing v4
|
|
101
|
+
* deposits it would otherwise fail to read.
|
|
110
102
|
*/
|
|
103
|
+
export declare const v4PulsechainContracts: V4Contracts;
|
|
111
104
|
/**
|
|
112
105
|
* V3 contracts deployed to Pulsechain (Chain ID 369)
|
|
113
106
|
* Deployed: 2026-01-12
|
|
@@ -132,6 +125,22 @@ export declare const V3_PULSECHAIN_DEPLOYMENT_REFERENCE: {
|
|
|
132
125
|
readonly nullifierRegistry: "0x4AeA2288ec542963256b66D70EfddF4cB8754f82";
|
|
133
126
|
readonly simpleAttestationVerifier: "0x49DAd1f29a3FE2E4D90a59e6687d9f7A2836B4a8";
|
|
134
127
|
};
|
|
128
|
+
/**
|
|
129
|
+
* V4 (EscrowV2 / OrchestratorV2) Pulsechain ancillary contracts - FOR REFERENCE ONLY
|
|
130
|
+
*
|
|
131
|
+
* Deployed on chain 369 on 2026-07-24 but NOT read by any runtime code path today
|
|
132
|
+
* (unlike V3_PULSECHAIN_DEPLOYMENT_REFERENCE, whose entries the escrow exposes
|
|
133
|
+
* dynamically). Recorded purely as an ops reference — a documented home for the
|
|
134
|
+
* addresses you need when making owner calls or wiring a future feature. Do NOT
|
|
135
|
+
* import these into runtime paths; if one ever gets called, add a typed entry to
|
|
136
|
+
* the version contract sets above instead.
|
|
137
|
+
*/
|
|
138
|
+
export declare const V4_PULSECHAIN_DEPLOYMENT_REFERENCE: {
|
|
139
|
+
readonly multiAttestationVerifier: "0xd98bdf683a8b218Ea45Cc8A258ef1A662ef8e9ae";
|
|
140
|
+
readonly addressGroupRegistry: "0x2e6E054eca4EC24149516AC8036Dd30c937237AF";
|
|
141
|
+
readonly whitelistPreIntentHookV2: "0x4F7f6691f8c362d31d19567133Ce33CC3EEd6d51";
|
|
142
|
+
readonly protocolViewerV2: "0x02CC2997B73A1d1E1D0151319F25125B11A55565";
|
|
143
|
+
};
|
|
135
144
|
export declare const versions: {
|
|
136
145
|
readonly V2: "v2";
|
|
137
146
|
readonly V3: "v3";
|
|
@@ -156,6 +165,7 @@ export declare const contracts: {
|
|
|
156
165
|
};
|
|
157
166
|
v4: {
|
|
158
167
|
8453: V4Contracts;
|
|
168
|
+
369: V4Contracts;
|
|
159
169
|
};
|
|
160
170
|
};
|
|
161
171
|
/**
|
|
@@ -170,9 +180,9 @@ export declare const getContracts: (version: Version, chainId: ChainId) => V2Con
|
|
|
170
180
|
*
|
|
171
181
|
* This is the routing decision for **new deposits**: a chain automatically
|
|
172
182
|
* moves to v4 the moment its v4 contracts are registered in
|
|
173
|
-
* `contracts.v4[chainId]` — no code change, no flag flip. Base
|
|
174
|
-
* today (EscrowV2/OrchestratorV2 are live
|
|
175
|
-
*
|
|
183
|
+
* `contracts.v4[chainId]` — no code change, no flag flip. Both Base and
|
|
184
|
+
* PulseChain return `'v4'` today (EscrowV2/OrchestratorV2 are live on both; the
|
|
185
|
+
* PulseChain deploy landed 2026-07-24).
|
|
176
186
|
*
|
|
177
187
|
* Distinct from {@link getLatestContract}'s `getDefaultVersion` fallback, which
|
|
178
188
|
* stays on v3 for read paths that aggregate across generations. Only deposit
|
|
@@ -182,10 +192,9 @@ export declare const getContracts: (version: Version, chainId: ChainId) => V2Con
|
|
|
182
192
|
* @returns The newest version with a configured escrow, or `'v3'` as a safe fallback
|
|
183
193
|
*
|
|
184
194
|
* @example
|
|
185
|
-
* //
|
|
195
|
+
* // Both chains have v4 contracts registered → new deposits go to v4
|
|
186
196
|
* getLatestConfiguredVersion(base.id) // 'v4'
|
|
187
|
-
* //
|
|
188
|
-
* getLatestConfiguredVersion(pulsechain.id) // 'v3'
|
|
197
|
+
* getLatestConfiguredVersion(pulsechain.id) // 'v4'
|
|
189
198
|
*/
|
|
190
199
|
export declare const getLatestConfiguredVersion: (chainId: ChainId) => Version;
|
|
191
200
|
/**
|
|
@@ -210,6 +219,100 @@ export declare const getLatestConfiguredVersion: (chainId: ChainId) => Version;
|
|
|
210
219
|
* const v2Escrow = getLatestContract(pulsechain.id, 'escrow', 'v2')
|
|
211
220
|
*/
|
|
212
221
|
export declare const getLatestContract: (chainId: ChainId, key: ContractKey, version?: Version) => Hex | null;
|
|
222
|
+
/**
|
|
223
|
+
* The UnifiedPaymentVerifier whose address belongs in the EIP-712 domain of a
|
|
224
|
+
* payment attestation on this chain.
|
|
225
|
+
*
|
|
226
|
+
* **Why this is not keyed on the deposit's generation.** A v3 deposit and a v4
|
|
227
|
+
* deposit do not get different verifiers. Both escrow generations point at the
|
|
228
|
+
* *same* `PaymentVerifierRegistry`, and the registry resolves a payment method
|
|
229
|
+
* to a verifier — so the verifier follows the registry's current mapping, never
|
|
230
|
+
* the escrow the deposit happens to live in. Verified on chain 369 on
|
|
231
|
+
* 2026-07-24: `escrow.paymentVerifierRegistry()` returns
|
|
232
|
+
* `0x6a4fF1950Be05995dEe75920cf27CD8FEbB6597A` for both the v3 escrow
|
|
233
|
+
* (`0x0B42…f11a`) and the v4 escrow (`0xD481…99e3`), and `getVerifier(...)` on
|
|
234
|
+
* that registry returns the v4 UnifiedPaymentVerifierV2
|
|
235
|
+
* (`0x8966f5428CD8893722BF5EbF74D44BE93F600796`) for **all 15** registered
|
|
236
|
+
* payment methods — including every method a v3 deposit uses.
|
|
237
|
+
*
|
|
238
|
+
* **Why it matters.** `UnifiedPaymentVerifier` builds its domain separator with
|
|
239
|
+
* `verifyingContract = address(this)`, so an attestation signed against the
|
|
240
|
+
* wrong verifier address produces a digest the on-chain check rejects. Pinning
|
|
241
|
+
* this to `'v3'` (as every call site used to) hands the signer the retired
|
|
242
|
+
* `0x0e73181a…` and every submission fails signature verification.
|
|
243
|
+
*
|
|
244
|
+
* Tracking the latest configured version keeps this correct through the next
|
|
245
|
+
* verifier generation too, for the same reason
|
|
246
|
+
* {@link getLatestConfiguredVersion} keeps deposit routing correct: registering
|
|
247
|
+
* the new addresses is the only step.
|
|
248
|
+
*
|
|
249
|
+
* @param chainId - The chain the intent lives on (use `deposit.chainId`).
|
|
250
|
+
* @returns The verifier address, or null when the chain has none configured.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* const verifyingContract = getPaymentAttestationVerifier(deposit.chainId)
|
|
255
|
+
* // chain 369 → 0x8966f5428CD8893722BF5EbF74D44BE93F600796
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
export declare const getPaymentAttestationVerifier: (chainId: ChainId) => Hex | null;
|
|
259
|
+
/**
|
|
260
|
+
* The outcome of checking a client-supplied `verifyingContract`.
|
|
261
|
+
*
|
|
262
|
+
* `unconfigured` and `mismatch` are kept apart because they mean different
|
|
263
|
+
* things operationally: the first is our own misconfiguration (a chain with no
|
|
264
|
+
* verifier registered), the second is a request asking us to sign for an
|
|
265
|
+
* address that is not the one this chain settles against.
|
|
266
|
+
*/
|
|
267
|
+
export type VerifyingContractCheck = {
|
|
268
|
+
ok: true;
|
|
269
|
+
verifyingContract: Hex;
|
|
270
|
+
} | {
|
|
271
|
+
ok: false;
|
|
272
|
+
reason: 'unconfigured';
|
|
273
|
+
expected: null;
|
|
274
|
+
received: Hex | undefined;
|
|
275
|
+
} | {
|
|
276
|
+
ok: false;
|
|
277
|
+
reason: 'mismatch';
|
|
278
|
+
expected: Hex;
|
|
279
|
+
received: Hex | undefined;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Check a client-supplied `verifyingContract` against the verifier this chain
|
|
283
|
+
* actually settles against, and refuse anything else.
|
|
284
|
+
*
|
|
285
|
+
* **Why a server-side check exists at all.** The attestation signers take
|
|
286
|
+
* `verifyingContract` from the request body and sign the EIP-712 digest built
|
|
287
|
+
* around it. Whatever address the caller names is what our key attests to. A
|
|
288
|
+
* stale browser bundle still holding the retired v3 verifier therefore gets a
|
|
289
|
+
* perfectly valid signature over the wrong domain — and the buyer discovers it
|
|
290
|
+
* only when `fulfillIntent` reverts on signature verification, *after* the
|
|
291
|
+
* fiat has left their bank. Nobody can refund that from our side.
|
|
292
|
+
*
|
|
293
|
+
* Fail closed: signing is the irreversible step, so an unrecognised chain or an
|
|
294
|
+
* unparseable address is a refusal, never a pass-through.
|
|
295
|
+
*
|
|
296
|
+
* Comparison is case-insensitive because the address arrives as JSON from a
|
|
297
|
+
* browser and may be checksummed, lowercased, or neither.
|
|
298
|
+
*
|
|
299
|
+
* @param chainId - The chain the attestation will settle on
|
|
300
|
+
* @param verifyingContract - The address the caller asked us to sign for
|
|
301
|
+
* @returns The configured address on success, or the reason for refusing
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* const check = checkPaymentAttestationVerifier({ chainId, verifyingContract })
|
|
306
|
+
* if (!check.ok) {
|
|
307
|
+
* res.status(400).json({ error: 'VERIFYING_CONTRACT_REJECTED', reason: check.reason })
|
|
308
|
+
* return
|
|
309
|
+
* }
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export declare const checkPaymentAttestationVerifier: ({ chainId, verifyingContract, }: {
|
|
313
|
+
chainId: number;
|
|
314
|
+
verifyingContract: Hex | undefined;
|
|
315
|
+
}) => VerifyingContractCheck;
|
|
213
316
|
/**
|
|
214
317
|
* Represents a unique escrow location identified by version, chain, and address
|
|
215
318
|
*/
|
|
@@ -234,6 +337,43 @@ export type EscrowLocation = {
|
|
|
234
337
|
* // ]
|
|
235
338
|
*/
|
|
236
339
|
export declare function getAllEscrowContracts(): EscrowLocation[];
|
|
340
|
+
/**
|
|
341
|
+
* Locate a known escrow by address: which generation and which chain it is.
|
|
342
|
+
*
|
|
343
|
+
* The address is the only thing a deposit row reliably carries, so this is how
|
|
344
|
+
* a call site with a `deposit` in hand answers "is this v3 or v4?" without a
|
|
345
|
+
* network read.
|
|
346
|
+
*
|
|
347
|
+
* @param escrow - The escrow contract address (case-insensitive).
|
|
348
|
+
* @returns Its location, or null when the address is not a configured escrow.
|
|
349
|
+
*/
|
|
350
|
+
export declare function getEscrowLocation(escrow: Hex): EscrowLocation | null;
|
|
351
|
+
/**
|
|
352
|
+
* The protocol generation an escrow address belongs to.
|
|
353
|
+
*
|
|
354
|
+
* @param escrow - The escrow contract address (case-insensitive).
|
|
355
|
+
* @returns The version, or null when the address is not a configured escrow.
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* detectVersionFromEscrow('0xD48184389e7FE8EFb4f16AE62d8eFad9145099e3') // 'v4'
|
|
359
|
+
*/
|
|
360
|
+
export declare function detectVersionFromEscrow(escrow: Hex): Version | null;
|
|
361
|
+
/**
|
|
362
|
+
* The Orchestrator that governs intents for a given escrow.
|
|
363
|
+
*
|
|
364
|
+
* **Only v4 can be answered from configuration, and only v4 has to be.** A v2
|
|
365
|
+
* or v3 escrow exposes `orchestrator()` and callers read it from the chain.
|
|
366
|
+
* EscrowV2 (v4) does not: it authorizes through an `OrchestratorRegistry` and
|
|
367
|
+
* has no `orchestrator()` function at all, so that call reverts. The v4
|
|
368
|
+
* orchestrator address is therefore held explicitly in the v4 contract sets and
|
|
369
|
+
* returned from here.
|
|
370
|
+
*
|
|
371
|
+
* @param escrow - The escrow contract address (case-insensitive).
|
|
372
|
+
* @returns The orchestrator address for a v4 escrow; null for any other
|
|
373
|
+
* generation (the caller should read `orchestrator()` on-chain instead) and
|
|
374
|
+
* for unknown addresses.
|
|
375
|
+
*/
|
|
376
|
+
export declare function getConfiguredOrchestratorForEscrow(escrow: Hex): Hex | null;
|
|
237
377
|
/**
|
|
238
378
|
* Get all V3 escrow contracts across all chains.
|
|
239
379
|
* Used for cross-chain nullifier checking to prevent double-spending.
|