@quaivault/sdk 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -3
- package/dist/index.cjs +717 -254
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +426 -31
- package/dist/index.d.ts +426 -31
- package/dist/index.js +719 -256
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.cjs
CHANGED
|
@@ -30,8 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
AbortError: () => AbortError,
|
|
33
34
|
ConfigError: () => ConfigError,
|
|
34
35
|
Connection: () => Connection,
|
|
36
|
+
DEFAULT_CONCURRENCY: () => DEFAULT_CONCURRENCY,
|
|
37
|
+
DEFAULT_TEXT_LIMIT: () => DEFAULT_TEXT_LIMIT,
|
|
35
38
|
ENV_VARS: () => ENV_VARS,
|
|
36
39
|
Erc1155Abi: () => Erc1155Abi,
|
|
37
40
|
Erc20Abi: () => Erc20Abi,
|
|
@@ -64,9 +67,11 @@ __export(src_exports, {
|
|
|
64
67
|
SaltMiningError: () => SaltMiningError,
|
|
65
68
|
SocialRecoveryModuleAbi: () => SocialRecoveryModuleAbi,
|
|
66
69
|
StaleProposalError: () => StaleProposalError,
|
|
70
|
+
TokenContract: () => TokenContract,
|
|
67
71
|
ValidationError: () => ValidationError,
|
|
68
72
|
Vault: () => Vault,
|
|
69
73
|
VaultContract: () => VaultContract,
|
|
74
|
+
ZERO_ADDRESS: () => ZERO_ADDRESS,
|
|
70
75
|
allowedActions: () => allowedActions,
|
|
71
76
|
assertQuaiAddress: () => assertQuaiAddress,
|
|
72
77
|
assertQuaiAddresses: () => assertQuaiAddresses,
|
|
@@ -75,6 +80,7 @@ __export(src_exports, {
|
|
|
75
80
|
computeBytecodeHash: () => computeBytecodeHash,
|
|
76
81
|
computeFullSalt: () => computeFullSalt,
|
|
77
82
|
connect: () => connect,
|
|
83
|
+
createWorkerThreadsStrategy: () => createWorkerThreadsStrategy,
|
|
78
84
|
decodeCall: () => decodeCall,
|
|
79
85
|
decodeMultiSendPayload: () => decodeMultiSendPayload,
|
|
80
86
|
decodeRevert: () => decodeRevert,
|
|
@@ -97,6 +103,7 @@ __export(src_exports, {
|
|
|
97
103
|
knownErrorSelectors: () => knownErrorSelectors,
|
|
98
104
|
loadBalances: () => loadBalances,
|
|
99
105
|
mainnet: () => mainnet,
|
|
106
|
+
mapPooled: () => mapPooled,
|
|
100
107
|
mineSalt: () => mineSalt,
|
|
101
108
|
minimumExpiration: () => minimumExpiration,
|
|
102
109
|
networks: () => networks,
|
|
@@ -105,6 +112,7 @@ __export(src_exports, {
|
|
|
105
112
|
recoveryCall: () => recoveryCall,
|
|
106
113
|
remediationFor: () => remediationFor,
|
|
107
114
|
resolveConfig: () => resolveConfig,
|
|
115
|
+
sanitizeText: () => sanitizeText,
|
|
108
116
|
selfCall: () => selfCall,
|
|
109
117
|
shardPrefixOf: () => shardPrefixOf,
|
|
110
118
|
syncStrategy: () => syncStrategy,
|
|
@@ -3464,6 +3472,11 @@ var QuaiVaultError = class extends Error {
|
|
|
3464
3472
|
};
|
|
3465
3473
|
}
|
|
3466
3474
|
};
|
|
3475
|
+
var AbortError = class extends QuaiVaultError {
|
|
3476
|
+
constructor(operation = "The operation") {
|
|
3477
|
+
super("ABORTED", `${operation} was aborted.`);
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
3467
3480
|
var ConfigError = class extends QuaiVaultError {
|
|
3468
3481
|
constructor(message, remediation) {
|
|
3469
3482
|
super("CONFIG", message, { remediation });
|
|
@@ -3582,6 +3595,16 @@ var Connection = class {
|
|
|
3582
3595
|
write ? this.requireSigner("This recovery write") : this.provider
|
|
3583
3596
|
);
|
|
3584
3597
|
}
|
|
3598
|
+
/**
|
|
3599
|
+
* Read-only handle to a token contract.
|
|
3600
|
+
*
|
|
3601
|
+
* No write variant: the SDK never calls a token directly. Moving tokens goes
|
|
3602
|
+
* through the vault's proposal flow, which encodes the transfer as calldata.
|
|
3603
|
+
*/
|
|
3604
|
+
token(address2, standard) {
|
|
3605
|
+
const abi = standard === "ERC20" ? Erc20Abi : standard === "ERC721" ? Erc721Abi : Erc1155Abi;
|
|
3606
|
+
return new import_quais.Contract(address2, abi, this.provider);
|
|
3607
|
+
}
|
|
3585
3608
|
};
|
|
3586
3609
|
function createPrivateKeySigner(privateKey, provider) {
|
|
3587
3610
|
const normalized = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
|
|
@@ -3624,6 +3647,36 @@ function normalizeTxHash(value, label = "transaction hash") {
|
|
|
3624
3647
|
// src/config/resolve.ts
|
|
3625
3648
|
var import_quais2 = require("quais");
|
|
3626
3649
|
|
|
3650
|
+
// src/lifecycle/status.ts
|
|
3651
|
+
function nowSeconds() {
|
|
3652
|
+
return Math.floor(Date.now() / 1e3);
|
|
3653
|
+
}
|
|
3654
|
+
function executableAfterOf(approvedAt, executionDelay) {
|
|
3655
|
+
return approvedAt > 0 ? approvedAt + executionDelay : 0;
|
|
3656
|
+
}
|
|
3657
|
+
function deriveStatus(state, at = nowSeconds()) {
|
|
3658
|
+
if (state.failed) return "failed";
|
|
3659
|
+
if (state.executed) return "executed";
|
|
3660
|
+
if (state.cancelled) return state.isExpired ? "expired" : "cancelled";
|
|
3661
|
+
if (state.expiration > 0 && at > state.expiration) return "expired";
|
|
3662
|
+
if (state.approvalCount < state.threshold) return "pending";
|
|
3663
|
+
const executableAfter = executableAfterOf(state.approvedAt, state.executionDelay);
|
|
3664
|
+
if (state.executionDelay > 0 && (state.approvedAt === 0 || at < executableAfter)) {
|
|
3665
|
+
return "timelocked";
|
|
3666
|
+
}
|
|
3667
|
+
return "ready";
|
|
3668
|
+
}
|
|
3669
|
+
function isTerminal(status) {
|
|
3670
|
+
return status === "executed" || status === "failed" || status === "cancelled" || status === "expired";
|
|
3671
|
+
}
|
|
3672
|
+
function deriveRecoveryStatus(state, at = nowSeconds()) {
|
|
3673
|
+
if (state.executed) return "executed";
|
|
3674
|
+
if (state.expiration > 0 && at > state.expiration) return "expired";
|
|
3675
|
+
if (state.approvalCount < state.requiredThreshold) return "pending";
|
|
3676
|
+
if (at < state.executionTime) return "timelocked";
|
|
3677
|
+
return "ready";
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3627
3680
|
// src/config/networks.ts
|
|
3628
3681
|
var INDEXER_URL = "https://xbftgyuxaxagptudledv.supabase.co";
|
|
3629
3682
|
var INDEXER_PUBLISHABLE_KEY = "sb_publishable_EO-sTB-jqUzlsiugOEks-Q_qRtHDaHU";
|
|
@@ -3818,9 +3871,14 @@ function resolveConfig(options = {}) {
|
|
|
3818
3871
|
contracts,
|
|
3819
3872
|
indexer,
|
|
3820
3873
|
rpcUrl,
|
|
3821
|
-
|
|
3874
|
+
// An explicit signer wins outright in `Connection`, so resolving a key here would
|
|
3875
|
+
// only pull a secret into memory that nothing can use. Skip it.
|
|
3876
|
+
privateKey: options.signer ? void 0 : pick(options.privateKey, env[ENV_VARS.privateKey]),
|
|
3822
3877
|
consistency,
|
|
3823
3878
|
maxIndexerLagBlocks: options.maxIndexerLagBlocks ?? parseLag(env[ENV_VARS.maxIndexerLagBlocks]) ?? 50,
|
|
3879
|
+
// Deliberately not environment-configurable: a clock is a function, and a numeric
|
|
3880
|
+
// offset in an env var would reintroduce the sign convention `Clock` exists to avoid.
|
|
3881
|
+
now: options.now ?? nowSeconds,
|
|
3824
3882
|
retry: {
|
|
3825
3883
|
...options.retry?.maxAttempts != null ? { maxAttempts: options.retry.maxAttempts } : {},
|
|
3826
3884
|
...options.retry?.baseDelayMs != null ? { baseDelayMs: options.retry.baseDelayMs } : {},
|
|
@@ -3948,7 +4006,7 @@ function isTransient(error) {
|
|
|
3948
4006
|
}
|
|
3949
4007
|
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
3950
4008
|
if (signal?.aborted) {
|
|
3951
|
-
reject(new
|
|
4009
|
+
reject(new AbortError("Waiting to retry"));
|
|
3952
4010
|
return;
|
|
3953
4011
|
}
|
|
3954
4012
|
const timer = setTimeout(() => {
|
|
@@ -3957,7 +4015,7 @@ var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
3957
4015
|
}, ms);
|
|
3958
4016
|
const onAbort = () => {
|
|
3959
4017
|
clearTimeout(timer);
|
|
3960
|
-
reject(new
|
|
4018
|
+
reject(new AbortError("Waiting to retry"));
|
|
3961
4019
|
};
|
|
3962
4020
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3963
4021
|
});
|
|
@@ -3967,7 +4025,7 @@ async function withRetry(fn, options = {}) {
|
|
|
3967
4025
|
const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;
|
|
3968
4026
|
let lastError;
|
|
3969
4027
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
3970
|
-
if (options.signal?.aborted) throw new
|
|
4028
|
+
if (options.signal?.aborted) throw new AbortError("The retried operation");
|
|
3971
4029
|
try {
|
|
3972
4030
|
return await fn();
|
|
3973
4031
|
} catch (error) {
|
|
@@ -4162,8 +4220,39 @@ var FactoryContract = class {
|
|
|
4162
4220
|
|
|
4163
4221
|
// src/errors/decode.ts
|
|
4164
4222
|
var import_quais4 = require("quais");
|
|
4223
|
+
|
|
4224
|
+
// src/text.ts
|
|
4225
|
+
var UNSAFE_CHARS = new RegExp(
|
|
4226
|
+
[
|
|
4227
|
+
"[\\u0000-\\u001F",
|
|
4228
|
+
// C0 controls, incl. ESC (0x1B) — the head of every ANSI sequence
|
|
4229
|
+
"\\u007F-\\u009F",
|
|
4230
|
+
// DEL and the C1 controls
|
|
4231
|
+
"\\u200B-\\u200F",
|
|
4232
|
+
// zero-width space/joiners, LRM, RLM
|
|
4233
|
+
"\\u202A-\\u202E",
|
|
4234
|
+
// bidi embeddings and overrides
|
|
4235
|
+
"\\u2060-\\u2064",
|
|
4236
|
+
// word joiner and the invisible operators
|
|
4237
|
+
"\\u2066-\\u2069",
|
|
4238
|
+
// bidi isolates
|
|
4239
|
+
"\\uFEFF]"
|
|
4240
|
+
// zero-width no-break space / BOM
|
|
4241
|
+
].join(""),
|
|
4242
|
+
"g"
|
|
4243
|
+
);
|
|
4244
|
+
var DEFAULT_TEXT_LIMIT = 128;
|
|
4245
|
+
function sanitizeText(value, maxLength = DEFAULT_TEXT_LIMIT) {
|
|
4246
|
+
if (typeof value !== "string") return "";
|
|
4247
|
+
const cleaned = value.replace(UNSAFE_CHARS, "").trim();
|
|
4248
|
+
if (cleaned.length <= maxLength) return cleaned;
|
|
4249
|
+
return `${cleaned.slice(0, Math.max(0, maxLength - 1))}\u2026`;
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
// src/errors/decode.ts
|
|
4165
4253
|
var SOLIDITY_ERROR_SELECTOR = "0x08c379a0";
|
|
4166
4254
|
var SOLIDITY_PANIC_SELECTOR = "0x4e487b71";
|
|
4255
|
+
var REVERT_TEXT_LIMIT = 256;
|
|
4167
4256
|
var registry = null;
|
|
4168
4257
|
function buildRegistry() {
|
|
4169
4258
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -4253,7 +4342,7 @@ function formatArgs(fragment, args) {
|
|
|
4253
4342
|
if (args.length === 0) return "";
|
|
4254
4343
|
const parts = fragment.inputs.map((input, i) => {
|
|
4255
4344
|
const value = args[i];
|
|
4256
|
-
const rendered = typeof value === "bigint" ? value.toString() : String(value);
|
|
4345
|
+
const rendered = typeof value === "bigint" ? value.toString() : typeof value === "string" ? sanitizeText(value, REVERT_TEXT_LIMIT) : String(value);
|
|
4257
4346
|
return input.name ? `${input.name}: ${rendered}` : rendered;
|
|
4258
4347
|
});
|
|
4259
4348
|
return `(${parts.join(", ")})`;
|
|
@@ -4269,7 +4358,7 @@ function decodeRevert(data) {
|
|
|
4269
4358
|
name: "Error",
|
|
4270
4359
|
args: [reason],
|
|
4271
4360
|
selector,
|
|
4272
|
-
message:
|
|
4361
|
+
message: sanitizeText(reason, REVERT_TEXT_LIMIT)
|
|
4273
4362
|
};
|
|
4274
4363
|
} catch {
|
|
4275
4364
|
return void 0;
|
|
@@ -4357,6 +4446,7 @@ var MAX_EXECUTION_DELAY = 30 * 24 * 60 * 60;
|
|
|
4357
4446
|
var MAX_OWNERS = 20;
|
|
4358
4447
|
var MAX_MODULES = 50;
|
|
4359
4448
|
var SENTINEL_MODULES = "0x0000000000000000000000000000000000000001";
|
|
4449
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
4360
4450
|
var selfCall = {
|
|
4361
4451
|
addOwner: (owner) => vault.encodeFunctionData("addOwner", [owner]),
|
|
4362
4452
|
removeOwner: (owner) => vault.encodeFunctionData("removeOwner", [owner]),
|
|
@@ -4463,7 +4553,7 @@ function encodeMultiSendPayload(calls) {
|
|
|
4463
4553
|
function encodeMultiSend(calls) {
|
|
4464
4554
|
return multiSend.encodeFunctionData("multiSend", [encodeMultiSendPayload(calls)]);
|
|
4465
4555
|
}
|
|
4466
|
-
function minimumExpiration(effectiveDelaySeconds, marginSeconds = 300, at =
|
|
4556
|
+
function minimumExpiration(effectiveDelaySeconds, marginSeconds = 300, at = nowSeconds()) {
|
|
4467
4557
|
return at + effectiveDelaySeconds + marginSeconds;
|
|
4468
4558
|
}
|
|
4469
4559
|
|
|
@@ -4524,7 +4614,7 @@ var syncStrategy = {
|
|
|
4524
4614
|
async mine({ factory, deployer, bytecodeHash, targetPrefix, maxAttempts, timeoutMs, onProgress, signal }) {
|
|
4525
4615
|
const startedAt = Date.now();
|
|
4526
4616
|
for (let attempts = 0; attempts < maxAttempts; ) {
|
|
4527
|
-
if (signal?.aborted) throw new
|
|
4617
|
+
if (signal?.aborted) throw new AbortError("Salt mining");
|
|
4528
4618
|
if (Date.now() - startedAt > timeoutMs) {
|
|
4529
4619
|
throw new SaltMiningError(
|
|
4530
4620
|
`Salt mining timed out after ${timeoutMs} ms (${attempts} attempts).`,
|
|
@@ -4552,109 +4642,127 @@ var syncStrategy = {
|
|
|
4552
4642
|
);
|
|
4553
4643
|
}
|
|
4554
4644
|
};
|
|
4555
|
-
var
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4645
|
+
var WorkersUnavailable = class extends Error {
|
|
4646
|
+
};
|
|
4647
|
+
async function loadNodeWorkers() {
|
|
4648
|
+
try {
|
|
4649
|
+
const { Worker } = await import("worker_threads");
|
|
4650
|
+
const os = await import("os");
|
|
4651
|
+
return { Worker, parallelism: (os.availableParallelism ?? (() => os.cpus().length))() };
|
|
4652
|
+
} catch {
|
|
4653
|
+
return null;
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
function createWorkerThreadsStrategy(load = loadNodeWorkers) {
|
|
4657
|
+
return {
|
|
4658
|
+
name: "worker_threads",
|
|
4659
|
+
async mine(job) {
|
|
4660
|
+
const runtime = await load();
|
|
4661
|
+
if (!runtime) return syncStrategy.mine(job);
|
|
4662
|
+
try {
|
|
4663
|
+
return await mineInWorkers(runtime.Worker, runtime.parallelism, job);
|
|
4664
|
+
} catch (err) {
|
|
4665
|
+
if (err instanceof WorkersUnavailable) return syncStrategy.mine(job);
|
|
4666
|
+
throw err;
|
|
4667
|
+
}
|
|
4566
4668
|
}
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4669
|
+
};
|
|
4670
|
+
}
|
|
4671
|
+
var workerThreadsStrategy = createWorkerThreadsStrategy();
|
|
4672
|
+
function mineInWorkers(Worker, parallelism, job) {
|
|
4673
|
+
const { factory, deployer, bytecodeHash, targetPrefix, maxAttempts, timeoutMs, onProgress, signal } = job;
|
|
4674
|
+
const workerCount = Math.max(1, Math.min(8, parallelism - 1));
|
|
4675
|
+
const perWorker = Math.ceil(maxAttempts / workerCount);
|
|
4676
|
+
const startedAt = Date.now();
|
|
4677
|
+
const workerSource = `
|
|
4678
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
4679
|
+
const { getCreate2Address, hexlify, isQuaiAddress, keccak256, randomBytes, solidityPacked } = require('quais');
|
|
4680
|
+
const { factory, deployer, bytecodeHash, targetPrefix, maxAttempts } = workerData;
|
|
4681
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
4682
|
+
const salt = hexlify(randomBytes(32));
|
|
4683
|
+
const fullSalt = keccak256(solidityPacked(['address', 'bytes32'], [deployer, salt]));
|
|
4684
|
+
const address = getCreate2Address(factory, fullSalt, bytecodeHash);
|
|
4685
|
+
if (address.toLowerCase().startsWith(targetPrefix) && isQuaiAddress(address)) {
|
|
4686
|
+
parentPort.postMessage({ type: 'result', salt, address, attempts: i + 1 });
|
|
4687
|
+
return;
|
|
4584
4688
|
}
|
|
4585
|
-
parentPort.postMessage({ type: '
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
const
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
)
|
|
4689
|
+
if ((i + 1) % ${PROGRESS_INTERVAL} === 0) parentPort.postMessage({ type: 'progress', attempts: i + 1 });
|
|
4690
|
+
}
|
|
4691
|
+
parentPort.postMessage({ type: 'exhausted', attempts: maxAttempts });
|
|
4692
|
+
`;
|
|
4693
|
+
return new Promise((resolve, reject) => {
|
|
4694
|
+
const workers = [];
|
|
4695
|
+
let settled = false;
|
|
4696
|
+
let exhausted = 0;
|
|
4697
|
+
let totalAttempts = 0;
|
|
4698
|
+
const progressByWorker = new Array(workerCount).fill(0);
|
|
4699
|
+
const cleanup = () => {
|
|
4700
|
+
clearTimeout(timer);
|
|
4701
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4702
|
+
for (const w of workers) void w.terminate();
|
|
4703
|
+
};
|
|
4704
|
+
const settle = (fn) => {
|
|
4705
|
+
if (settled) return;
|
|
4706
|
+
settled = true;
|
|
4707
|
+
cleanup();
|
|
4708
|
+
fn();
|
|
4709
|
+
};
|
|
4710
|
+
const timer = setTimeout(
|
|
4711
|
+
() => settle(
|
|
4712
|
+
() => reject(
|
|
4713
|
+
new SaltMiningError(
|
|
4714
|
+
`Salt mining timed out after ${timeoutMs} ms (~${totalAttempts} attempts across ${workerCount} workers).`,
|
|
4715
|
+
"Raise timeoutMs or maxAttempts."
|
|
4611
4716
|
)
|
|
4612
|
-
)
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4717
|
+
)
|
|
4718
|
+
),
|
|
4719
|
+
timeoutMs
|
|
4720
|
+
);
|
|
4721
|
+
const onAbort = () => settle(() => reject(new AbortError("Salt mining")));
|
|
4722
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4723
|
+
for (let i = 0; i < workerCount; i++) {
|
|
4724
|
+
let worker;
|
|
4725
|
+
try {
|
|
4726
|
+
worker = new Worker(workerSource, {
|
|
4619
4727
|
eval: true,
|
|
4620
4728
|
workerData: { factory, deployer, bytecodeHash, targetPrefix, maxAttempts: perWorker }
|
|
4621
4729
|
});
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4730
|
+
} catch (err) {
|
|
4731
|
+
settle(() => reject(new WorkersUnavailable(String(err))));
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
workers.push(worker);
|
|
4735
|
+
worker.on("message", (msg) => {
|
|
4736
|
+
if (msg.type === "progress") {
|
|
4737
|
+
progressByWorker[i] = msg.attempts ?? 0;
|
|
4738
|
+
totalAttempts = progressByWorker.reduce((a, b) => a + b, 0);
|
|
4739
|
+
onProgress?.(totalAttempts);
|
|
4740
|
+
} else if (msg.type === "result") {
|
|
4741
|
+
settle(
|
|
4742
|
+
() => resolve({
|
|
4743
|
+
salt: msg.salt,
|
|
4744
|
+
predictedAddress: msg.address,
|
|
4745
|
+
attempts: totalAttempts + (msg.attempts ?? 0),
|
|
4746
|
+
durationMs: Date.now() - startedAt
|
|
4747
|
+
})
|
|
4748
|
+
);
|
|
4749
|
+
} else if (msg.type === "exhausted") {
|
|
4750
|
+
if (++exhausted === workerCount) {
|
|
4629
4751
|
settle(
|
|
4630
|
-
() =>
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
durationMs: Date.now() - startedAt
|
|
4635
|
-
})
|
|
4636
|
-
);
|
|
4637
|
-
} else if (msg.type === "exhausted") {
|
|
4638
|
-
if (++exhausted === workerCount) {
|
|
4639
|
-
settle(
|
|
4640
|
-
() => reject(
|
|
4641
|
-
new SaltMiningError(
|
|
4642
|
-
`No valid salt found for prefix ${targetPrefix} after ${maxAttempts} attempts.`,
|
|
4643
|
-
"Raise maxAttempts, or confirm the deployer address is on the shard you expect."
|
|
4644
|
-
)
|
|
4752
|
+
() => reject(
|
|
4753
|
+
new SaltMiningError(
|
|
4754
|
+
`No valid salt found for prefix ${targetPrefix} after ${maxAttempts} attempts.`,
|
|
4755
|
+
"Raise maxAttempts, or confirm the deployer address is on the shard you expect."
|
|
4645
4756
|
)
|
|
4646
|
-
)
|
|
4647
|
-
|
|
4757
|
+
)
|
|
4758
|
+
);
|
|
4648
4759
|
}
|
|
4649
|
-
}
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
});
|
|
4656
|
-
}
|
|
4657
|
-
};
|
|
4760
|
+
}
|
|
4761
|
+
});
|
|
4762
|
+
worker.on("error", (err) => settle(() => reject(new WorkersUnavailable(err.message))));
|
|
4763
|
+
}
|
|
4764
|
+
});
|
|
4765
|
+
}
|
|
4658
4766
|
function defaultStrategy() {
|
|
4659
4767
|
const hasNode = typeof globalThis.process?.versions?.node === "string";
|
|
4660
4768
|
return hasNode ? workerThreadsStrategy : syncStrategy;
|
|
@@ -4876,7 +4984,7 @@ function validateCreateParams(params) {
|
|
|
4876
4984
|
owners.forEach((owner, i) => {
|
|
4877
4985
|
assertQuaiAddress(owner, `owner[${i}]`);
|
|
4878
4986
|
const key = owner.toLowerCase();
|
|
4879
|
-
if (key ===
|
|
4987
|
+
if (key === ZERO_ADDRESS || key === SENTINEL_MODULES) {
|
|
4880
4988
|
throw new ValidationError(
|
|
4881
4989
|
`Owner ${owner} is reserved \u2014 the zero address and the module sentinel 0x\u202601 are rejected.`
|
|
4882
4990
|
);
|
|
@@ -4885,6 +4993,11 @@ function validateCreateParams(params) {
|
|
|
4885
4993
|
seen.add(key);
|
|
4886
4994
|
});
|
|
4887
4995
|
if (params.initialModules?.length) {
|
|
4996
|
+
if (params.initialModules.length > MAX_MODULES) {
|
|
4997
|
+
throw new ValidationError(
|
|
4998
|
+
`A vault may have at most ${MAX_MODULES} modules (got ${params.initialModules.length}).`
|
|
4999
|
+
);
|
|
5000
|
+
}
|
|
4888
5001
|
assertQuaiAddresses(params.initialModules, "initialModules");
|
|
4889
5002
|
}
|
|
4890
5003
|
if (params.initialDelegatecallTargets?.length) {
|
|
@@ -4922,7 +5035,8 @@ function extractCreatedVault(receipt, factoryAddress) {
|
|
|
4922
5035
|
}
|
|
4923
5036
|
|
|
4924
5037
|
// src/indexer/client.ts
|
|
4925
|
-
var
|
|
5038
|
+
var import_postgrest_js = require("@supabase/postgrest-js");
|
|
5039
|
+
var import_realtime_js = require("@supabase/realtime-js");
|
|
4926
5040
|
|
|
4927
5041
|
// src/indexer/schemas.ts
|
|
4928
5042
|
var schemas_exports = {};
|
|
@@ -5121,30 +5235,63 @@ function toNumber(value, fallback = 0) {
|
|
|
5121
5235
|
}
|
|
5122
5236
|
|
|
5123
5237
|
// src/indexer/client.ts
|
|
5124
|
-
var SDK_VERSION = "0.1
|
|
5238
|
+
var SDK_VERSION = true ? "0.2.1" : "dev";
|
|
5125
5239
|
var IndexerClient = class {
|
|
5126
5240
|
config;
|
|
5127
5241
|
client;
|
|
5242
|
+
realtime = null;
|
|
5128
5243
|
healthCache = null;
|
|
5129
5244
|
inflightHealth = null;
|
|
5130
|
-
/**
|
|
5245
|
+
/**
|
|
5246
|
+
* Health results are reused for this long to keep read paths cheap.
|
|
5247
|
+
*
|
|
5248
|
+
* This and `waitForBlock`'s deadline are elapsed-time arithmetic, so they use the
|
|
5249
|
+
* raw local clock rather than `ClientOptions.now` — see the note in `chain/retry.ts`.
|
|
5250
|
+
*/
|
|
5131
5251
|
healthCacheMs;
|
|
5132
5252
|
constructor(config, options = {}) {
|
|
5133
5253
|
this.config = config;
|
|
5134
5254
|
this.healthCacheMs = options.healthCacheMs ?? 5e3;
|
|
5135
|
-
this.client =
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5255
|
+
this.client = new import_postgrest_js.PostgrestClient(`${baseUrl(config.url)}/rest/v1`, {
|
|
5256
|
+
headers: {
|
|
5257
|
+
apikey: config.anonKey,
|
|
5258
|
+
Authorization: `Bearer ${config.anonKey}`,
|
|
5259
|
+
"x-client-info": `quaivault-sdk/${SDK_VERSION}`
|
|
5260
|
+
},
|
|
5261
|
+
schema: config.schema
|
|
5139
5262
|
});
|
|
5140
5263
|
}
|
|
5141
|
-
/**
|
|
5142
|
-
get
|
|
5264
|
+
/** The PostgREST client, for queries the SDK does not wrap. */
|
|
5265
|
+
get rest() {
|
|
5143
5266
|
return this.client;
|
|
5144
5267
|
}
|
|
5145
5268
|
from(table) {
|
|
5146
5269
|
return this.client.from(table);
|
|
5147
5270
|
}
|
|
5271
|
+
/**
|
|
5272
|
+
* Realtime client, constructed on first use.
|
|
5273
|
+
*
|
|
5274
|
+
* Lazy because it opens a WebSocket and starts a heartbeat, and the large majority
|
|
5275
|
+
* of SDK usage never calls `watch()`. Nothing here should cost a socket unless
|
|
5276
|
+
* somebody asked to follow a vault.
|
|
5277
|
+
*/
|
|
5278
|
+
realtimeClient() {
|
|
5279
|
+
if (!this.realtime) {
|
|
5280
|
+
this.realtime = new import_realtime_js.RealtimeClient(
|
|
5281
|
+
`${baseUrl(this.config.url).replace(/^http/, "ws")}/realtime/v1`,
|
|
5282
|
+
{ params: { apikey: this.config.anonKey } }
|
|
5283
|
+
);
|
|
5284
|
+
}
|
|
5285
|
+
return this.realtime;
|
|
5286
|
+
}
|
|
5287
|
+
/** Open a Realtime channel. See `watchVault`, which is what callers should use. */
|
|
5288
|
+
channel(name) {
|
|
5289
|
+
return this.realtimeClient().channel(name);
|
|
5290
|
+
}
|
|
5291
|
+
/** Close a channel opened by {@link channel}. */
|
|
5292
|
+
async removeChannel(channel) {
|
|
5293
|
+
await this.realtimeClient().removeChannel(channel);
|
|
5294
|
+
}
|
|
5148
5295
|
/**
|
|
5149
5296
|
* Run a query, parse each row, and surface failures as `IndexerQueryError`.
|
|
5150
5297
|
*
|
|
@@ -5162,6 +5309,36 @@ var IndexerClient = class {
|
|
|
5162
5309
|
}
|
|
5163
5310
|
return (data ?? []).map((row) => schema.parse(row));
|
|
5164
5311
|
}
|
|
5312
|
+
/**
|
|
5313
|
+
* Paged select: an exact `hasMore`, an approximate `total`.
|
|
5314
|
+
*
|
|
5315
|
+
* Both halves of that are deliberate. An `exact` count makes Postgres scan every
|
|
5316
|
+
* matching row on every page request, which is invisible on a young vault and
|
|
5317
|
+
* quadratic on a busy one — so the count is `estimated`, cheap and good enough for
|
|
5318
|
+
* "roughly how much history is there".
|
|
5319
|
+
*
|
|
5320
|
+
* But `hasMore` derived from an estimate would be a lie a caller can act on: a
|
|
5321
|
+
* paging loop that stops early drops real rows. So callers request one row beyond
|
|
5322
|
+
* the page and this trims it, which answers "is there another page" exactly, for
|
|
5323
|
+
* the cost of a single extra row.
|
|
5324
|
+
*/
|
|
5325
|
+
async selectPage(table, schema, limit, build) {
|
|
5326
|
+
const { data, error, count } = await build(this.client.from(table));
|
|
5327
|
+
if (error) {
|
|
5328
|
+
throw new IndexerQueryError(`Indexer query on "${table}" failed: ${error.message}`, error);
|
|
5329
|
+
}
|
|
5330
|
+
const rows = data ?? [];
|
|
5331
|
+
const hasMore = rows.length > limit;
|
|
5332
|
+
const parsed = (hasMore ? rows.slice(0, limit) : rows).map(
|
|
5333
|
+
(row) => schema.parse(row)
|
|
5334
|
+
);
|
|
5335
|
+
return {
|
|
5336
|
+
data: parsed,
|
|
5337
|
+
// Never report a total below what was just handed out, however the estimate lands.
|
|
5338
|
+
total: Math.max(count ?? 0, parsed.length),
|
|
5339
|
+
hasMore
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
5165
5342
|
/** Single-row variant. Returns null for PostgREST's "no rows" code. */
|
|
5166
5343
|
async selectOne(table, schema, build) {
|
|
5167
5344
|
const { data, error } = await build(this.client.from(table));
|
|
@@ -5216,7 +5393,7 @@ var IndexerClient = class {
|
|
|
5216
5393
|
const pollIntervalMs = options.pollIntervalMs ?? 1500;
|
|
5217
5394
|
const deadline = Date.now() + timeoutMs;
|
|
5218
5395
|
for (; ; ) {
|
|
5219
|
-
if (options.signal?.aborted) throw new
|
|
5396
|
+
if (options.signal?.aborted) throw new AbortError("Waiting for the indexer");
|
|
5220
5397
|
const state = await this.state();
|
|
5221
5398
|
const head = state?.lastIndexedBlock ?? 0;
|
|
5222
5399
|
if (head >= blockNumber) return { reached: true, lastIndexedBlock: head };
|
|
@@ -5228,12 +5405,12 @@ var IndexerClient = class {
|
|
|
5228
5405
|
}
|
|
5229
5406
|
async probeHealth() {
|
|
5230
5407
|
if (this.config.healthUrl) {
|
|
5408
|
+
const controller = new AbortController();
|
|
5409
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
5231
5410
|
try {
|
|
5232
|
-
const controller = new AbortController();
|
|
5233
|
-
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
5234
5411
|
const res = await fetch(new URL("/health", this.config.healthUrl), {
|
|
5235
5412
|
signal: controller.signal
|
|
5236
|
-
})
|
|
5413
|
+
});
|
|
5237
5414
|
const body = await res.json();
|
|
5238
5415
|
const d = body.details ?? {};
|
|
5239
5416
|
return {
|
|
@@ -5245,6 +5422,8 @@ var IndexerClient = class {
|
|
|
5245
5422
|
...res.ok ? {} : { error: `health endpoint returned ${res.status}` }
|
|
5246
5423
|
};
|
|
5247
5424
|
} catch {
|
|
5425
|
+
} finally {
|
|
5426
|
+
clearTimeout(timer);
|
|
5248
5427
|
}
|
|
5249
5428
|
}
|
|
5250
5429
|
try {
|
|
@@ -5267,10 +5446,14 @@ var IndexerClient = class {
|
|
|
5267
5446
|
}
|
|
5268
5447
|
}
|
|
5269
5448
|
};
|
|
5449
|
+
function baseUrl(url) {
|
|
5450
|
+
return url.replace(/\/+$/, "");
|
|
5451
|
+
}
|
|
5270
5452
|
|
|
5271
5453
|
// src/indexer/queries.ts
|
|
5272
5454
|
var DEFAULT_LIMIT = 50;
|
|
5273
5455
|
var MAX_LIMIT = 200;
|
|
5456
|
+
var CONFIRMATION_CHUNK = 40;
|
|
5274
5457
|
function lower(address2) {
|
|
5275
5458
|
return address2.toLowerCase();
|
|
5276
5459
|
}
|
|
@@ -5329,14 +5512,40 @@ var IndexerQueries = class {
|
|
|
5329
5512
|
(q) => q.select("*").eq("wallet_address", lower(vault2)).eq("tx_hash", txHash.toLowerCase()).single()
|
|
5330
5513
|
);
|
|
5331
5514
|
}
|
|
5515
|
+
/**
|
|
5516
|
+
* Several transactions by hash, chunked for the same reasons as
|
|
5517
|
+
* {@link activeConfirmationsBatch} — hashes are long, and they go in the query string.
|
|
5518
|
+
*
|
|
5519
|
+
* Rows come back in no particular order and hashes with no row are simply absent;
|
|
5520
|
+
* the caller matches them up.
|
|
5521
|
+
*/
|
|
5522
|
+
async transactionsByHash(vault2, txHashes) {
|
|
5523
|
+
if (txHashes.length === 0) return [];
|
|
5524
|
+
const hashes = txHashes.map((h) => h.toLowerCase());
|
|
5525
|
+
const chunks = [];
|
|
5526
|
+
for (let i = 0; i < hashes.length; i += CONFIRMATION_CHUNK) {
|
|
5527
|
+
chunks.push(hashes.slice(i, i + CONFIRMATION_CHUNK));
|
|
5528
|
+
}
|
|
5529
|
+
const pages = await Promise.all(
|
|
5530
|
+
chunks.map(
|
|
5531
|
+
(chunk) => this.client.select(
|
|
5532
|
+
"transactions",
|
|
5533
|
+
TransactionSchema,
|
|
5534
|
+
(q) => q.select("*").eq("wallet_address", lower(vault2)).in("tx_hash", chunk)
|
|
5535
|
+
)
|
|
5536
|
+
)
|
|
5537
|
+
);
|
|
5538
|
+
return pages.flat();
|
|
5539
|
+
}
|
|
5332
5540
|
async transactionHistory(vault2, options = {}) {
|
|
5333
5541
|
const { limit, offset } = bounds(options);
|
|
5334
5542
|
const statuses = options.status ?? ["executed", "cancelled", "expired", "failed"];
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5543
|
+
return this.client.selectPage(
|
|
5544
|
+
"transactions",
|
|
5545
|
+
TransactionSchema,
|
|
5546
|
+
limit,
|
|
5547
|
+
(q) => q.select("*", { count: "estimated" }).eq("wallet_address", lower(vault2)).in("status", statuses).order("submitted_at_block", { ascending: false }).range(offset, offset + limit)
|
|
5548
|
+
);
|
|
5340
5549
|
}
|
|
5341
5550
|
/** All confirmations for one transaction, including revoked ones. */
|
|
5342
5551
|
async confirmations(vault2, txHash) {
|
|
@@ -5347,26 +5556,41 @@ var IndexerQueries = class {
|
|
|
5347
5556
|
);
|
|
5348
5557
|
}
|
|
5349
5558
|
/**
|
|
5350
|
-
* Active confirmations for many transactions in
|
|
5559
|
+
* Active confirmations for many transactions, in as few round trips as is safe.
|
|
5351
5560
|
*
|
|
5352
5561
|
* "Active" here means not revoked. It does NOT mean the confirming address is
|
|
5353
5562
|
* still an owner — the indexer does not deactivate confirmations when an owner is
|
|
5354
5563
|
* removed, while the contract invalidates them via approval epochs. Callers must
|
|
5355
5564
|
* intersect with the active owner set; `Vault` does this for you.
|
|
5565
|
+
*
|
|
5566
|
+
* Chunked rather than issued as one `in(...)`, because a single filter fails two
|
|
5567
|
+
* ways at scale. PostgREST reads filters from the query string, and each 32-byte
|
|
5568
|
+
* hash costs ~70 characters there — a full {@link MAX_LIMIT} page of hashes builds a
|
|
5569
|
+
* ~14 KB request line, past the 8 KB cap most reverse proxies apply by default. The
|
|
5570
|
+
* response is bounded too: {@link CONFIRMATION_CHUNK} transactions can carry at most
|
|
5571
|
+
* `chunk × MAX_OWNERS` rows, and the chunk is sized to keep that under the
|
|
5572
|
+
* `max-rows` ceiling PostgREST deployments commonly set, so a busy vault cannot be
|
|
5573
|
+
* silently short-served.
|
|
5356
5574
|
*/
|
|
5357
5575
|
async activeConfirmationsBatch(vault2, txHashes) {
|
|
5358
5576
|
const result = /* @__PURE__ */ new Map();
|
|
5359
5577
|
for (const hash of txHashes) result.set(hash.toLowerCase(), []);
|
|
5360
5578
|
if (txHashes.length === 0) return result;
|
|
5361
|
-
const
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5579
|
+
const hashes = txHashes.map((h) => h.toLowerCase());
|
|
5580
|
+
const chunks = [];
|
|
5581
|
+
for (let i = 0; i < hashes.length; i += CONFIRMATION_CHUNK) {
|
|
5582
|
+
chunks.push(hashes.slice(i, i + CONFIRMATION_CHUNK));
|
|
5583
|
+
}
|
|
5584
|
+
const pages = await Promise.all(
|
|
5585
|
+
chunks.map(
|
|
5586
|
+
(chunk) => this.client.select(
|
|
5587
|
+
"confirmations",
|
|
5588
|
+
ConfirmationSchema,
|
|
5589
|
+
(q) => q.select("*").eq("wallet_address", lower(vault2)).in("tx_hash", chunk).eq("is_active", true)
|
|
5590
|
+
)
|
|
5591
|
+
)
|
|
5368
5592
|
);
|
|
5369
|
-
for (const row of
|
|
5593
|
+
for (const row of pages.flat()) {
|
|
5370
5594
|
const key = row.tx_hash.toLowerCase();
|
|
5371
5595
|
const list = result.get(key);
|
|
5372
5596
|
if (list) list.push(row);
|
|
@@ -5394,19 +5618,52 @@ var IndexerQueries = class {
|
|
|
5394
5618
|
// ---- value movement ------------------------------------------------------
|
|
5395
5619
|
async deposits(vault2, options = {}) {
|
|
5396
5620
|
const { limit, offset } = bounds(options);
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5621
|
+
return this.client.selectPage(
|
|
5622
|
+
"deposits",
|
|
5623
|
+
DepositSchema,
|
|
5624
|
+
limit,
|
|
5625
|
+
(q) => q.select("*", { count: "estimated" }).eq("wallet_address", lower(vault2)).order("deposited_at_block", { ascending: false }).range(offset, offset + limit)
|
|
5626
|
+
);
|
|
5402
5627
|
}
|
|
5403
5628
|
async tokenTransfers(vault2, options = {}) {
|
|
5404
5629
|
const { limit, offset } = bounds(options);
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5630
|
+
return this.client.selectPage(
|
|
5631
|
+
"token_transfers",
|
|
5632
|
+
TokenTransferSchema,
|
|
5633
|
+
limit,
|
|
5634
|
+
(q) => q.select("*", { count: "estimated" }).eq("wallet_address", lower(vault2)).order("block_number", { ascending: false }).range(offset, offset + limit)
|
|
5635
|
+
);
|
|
5636
|
+
}
|
|
5637
|
+
/**
|
|
5638
|
+
* Scan up to `budget` of the most recent transfer rows, paging past {@link MAX_LIMIT}.
|
|
5639
|
+
*
|
|
5640
|
+
* `tokenTransfers` clamps a single request, so a caller asking for more than the cap
|
|
5641
|
+
* silently received a short page — and then computed "did I see everything?" from
|
|
5642
|
+
* that short page's `hasMore`, which reported truncation the caller had not actually
|
|
5643
|
+
* hit. Paging here keeps the caller's budget meaningful and makes the returned
|
|
5644
|
+
* `hasMore` mean what it says: rows exist beyond the budget that was scanned.
|
|
5645
|
+
*
|
|
5646
|
+
* Pages by offset over a descending order, so a transfer landing mid-scan can shift
|
|
5647
|
+
* rows by one. That is acceptable for token *discovery* — a duplicate collapses into
|
|
5648
|
+
* the same map key and a missed row costs at most one candidate that the next call
|
|
5649
|
+
* picks up. Do not reuse this for anything that must see each row exactly once.
|
|
5650
|
+
*/
|
|
5651
|
+
async tokenTransferScan(vault2, budget) {
|
|
5652
|
+
const target = Math.max(1, Math.floor(budget));
|
|
5653
|
+
const data = [];
|
|
5654
|
+
let total = 0;
|
|
5655
|
+
let hasMore = false;
|
|
5656
|
+
while (data.length < target) {
|
|
5657
|
+
const page = await this.tokenTransfers(vault2, {
|
|
5658
|
+
limit: Math.min(MAX_LIMIT, target - data.length),
|
|
5659
|
+
offset: data.length
|
|
5660
|
+
});
|
|
5661
|
+
data.push(...page.data);
|
|
5662
|
+
total = page.total;
|
|
5663
|
+
hasMore = page.hasMore;
|
|
5664
|
+
if (page.data.length === 0 || !page.hasMore) break;
|
|
5665
|
+
}
|
|
5666
|
+
return { data, total, hasMore };
|
|
5410
5667
|
}
|
|
5411
5668
|
async tokens(addresses) {
|
|
5412
5669
|
if (addresses.length === 0) return [];
|
|
@@ -5598,39 +5855,7 @@ var RecoveryContract = class {
|
|
|
5598
5855
|
}
|
|
5599
5856
|
};
|
|
5600
5857
|
|
|
5601
|
-
// src/lifecycle/status.ts
|
|
5602
|
-
function nowSeconds() {
|
|
5603
|
-
return Math.floor(Date.now() / 1e3);
|
|
5604
|
-
}
|
|
5605
|
-
function executableAfterOf(approvedAt, executionDelay) {
|
|
5606
|
-
return approvedAt > 0 ? approvedAt + executionDelay : 0;
|
|
5607
|
-
}
|
|
5608
|
-
function deriveStatus(state, at = nowSeconds()) {
|
|
5609
|
-
if (state.failed) return "failed";
|
|
5610
|
-
if (state.executed) return "executed";
|
|
5611
|
-
if (state.cancelled) return state.isExpired ? "expired" : "cancelled";
|
|
5612
|
-
if (state.expiration > 0 && at > state.expiration) return "expired";
|
|
5613
|
-
if (state.approvalCount < state.threshold) return "pending";
|
|
5614
|
-
const executableAfter = executableAfterOf(state.approvedAt, state.executionDelay);
|
|
5615
|
-
if (state.executionDelay > 0 && (state.approvedAt === 0 || at < executableAfter)) {
|
|
5616
|
-
return "timelocked";
|
|
5617
|
-
}
|
|
5618
|
-
return "ready";
|
|
5619
|
-
}
|
|
5620
|
-
function isTerminal(status) {
|
|
5621
|
-
return status === "executed" || status === "failed" || status === "cancelled" || status === "expired";
|
|
5622
|
-
}
|
|
5623
|
-
function deriveRecoveryStatus(state, at = nowSeconds()) {
|
|
5624
|
-
if (state.executed) return "executed";
|
|
5625
|
-
if (state.expiration > 0 && at > state.expiration) return "expired";
|
|
5626
|
-
if (state.approvalCount < state.requiredThreshold) return "pending";
|
|
5627
|
-
if (at < state.executionTime) return "timelocked";
|
|
5628
|
-
return "ready";
|
|
5629
|
-
}
|
|
5630
|
-
|
|
5631
5858
|
// src/recovery.ts
|
|
5632
|
-
var SENTINEL = "0x0000000000000000000000000000000000000001";
|
|
5633
|
-
var ZERO = "0x0000000000000000000000000000000000000000";
|
|
5634
5859
|
var RecoveryModule = class {
|
|
5635
5860
|
constructor(ctx) {
|
|
5636
5861
|
this.ctx = ctx;
|
|
@@ -5652,14 +5877,27 @@ var RecoveryModule = class {
|
|
|
5652
5877
|
get vault() {
|
|
5653
5878
|
return this.ctx.vaultAddress;
|
|
5654
5879
|
}
|
|
5880
|
+
/**
|
|
5881
|
+
* The vault this module acts on, through the same retrying facade every other vault
|
|
5882
|
+
* read in the SDK uses. Recovery reaches into the vault for three checks — is the
|
|
5883
|
+
* module enabled, is the caller an owner, who are the current owners — and each of
|
|
5884
|
+
* them gates a write, so none of them should be the one read that silently skips
|
|
5885
|
+
* transient-failure handling.
|
|
5886
|
+
*/
|
|
5887
|
+
/** Now, in Unix seconds, for anything compared against a chain timestamp. */
|
|
5888
|
+
now() {
|
|
5889
|
+
return (this.ctx.now ?? nowSeconds)();
|
|
5890
|
+
}
|
|
5891
|
+
vaultContract() {
|
|
5892
|
+
return new VaultContract(this.ctx.connection.vault(this.vault), this.ctx.connection.retry);
|
|
5893
|
+
}
|
|
5655
5894
|
// =========================================================================
|
|
5656
5895
|
// Reads
|
|
5657
5896
|
// =========================================================================
|
|
5658
5897
|
/** Whether the module is currently enabled on the vault. */
|
|
5659
5898
|
async isEnabled() {
|
|
5660
5899
|
if (!this.ctx.moduleAddress) return false;
|
|
5661
|
-
|
|
5662
|
-
return vault2.getFunction("isModuleEnabled(address)")(this.address);
|
|
5900
|
+
return this.vaultContract().isModuleEnabled(this.address);
|
|
5663
5901
|
}
|
|
5664
5902
|
async config() {
|
|
5665
5903
|
const raw = await this.contract().getRecoveryConfig(this.vault);
|
|
@@ -5717,7 +5955,7 @@ var RecoveryModule = class {
|
|
|
5717
5955
|
*/
|
|
5718
5956
|
async history(options = {}) {
|
|
5719
5957
|
const queries = this.ctx.queries;
|
|
5720
|
-
if (!queries)
|
|
5958
|
+
if (!queries) throw new NoIndexerError("Listing recovery history");
|
|
5721
5959
|
const rows = await queries.recoveryHistory(this.vault, options);
|
|
5722
5960
|
return rows.map((row) => ({
|
|
5723
5961
|
hash: row.recovery_hash,
|
|
@@ -5737,7 +5975,7 @@ var RecoveryModule = class {
|
|
|
5737
5975
|
requiredThreshold: row.required_threshold,
|
|
5738
5976
|
executionTime: Number(row.execution_time),
|
|
5739
5977
|
expiration: Number(row.expiration ?? 0)
|
|
5740
|
-
}),
|
|
5978
|
+
}, this.now()),
|
|
5741
5979
|
executed: row.status === "executed",
|
|
5742
5980
|
initiator: (0, import_quais9.getAddress)(row.initiator_address),
|
|
5743
5981
|
source: "indexer"
|
|
@@ -5846,21 +6084,20 @@ var RecoveryModule = class {
|
|
|
5846
6084
|
`Not enough guardian approvals: ${request.approvalCount} of ${request.requiredThreshold}.`
|
|
5847
6085
|
);
|
|
5848
6086
|
}
|
|
5849
|
-
const now =
|
|
6087
|
+
const now = this.now();
|
|
5850
6088
|
if (now < request.executionTime) {
|
|
5851
6089
|
throw new PreconditionError(
|
|
5852
6090
|
`The recovery period has not elapsed. Executable after ${new Date(request.executionTime * 1e3).toISOString()}.`,
|
|
5853
6091
|
{ retryableAt: request.executionTime }
|
|
5854
6092
|
);
|
|
5855
6093
|
}
|
|
5856
|
-
const
|
|
5857
|
-
const currentOwners = await vault2.getFunction("getOwners()")();
|
|
6094
|
+
const currentOwners = await this.vaultContract().getOwners();
|
|
5858
6095
|
const current = new Set(Array.from(currentOwners).map((o) => String(o).toLowerCase()));
|
|
5859
6096
|
const overlap = request.newOwners.filter((o) => current.has(o.toLowerCase())).length;
|
|
5860
6097
|
const peak = current.size + request.newOwners.length - overlap;
|
|
5861
|
-
if (peak >
|
|
6098
|
+
if (peak > MAX_OWNERS) {
|
|
5862
6099
|
throw new PreconditionError(
|
|
5863
|
-
`Executing this recovery would transiently hold ${peak} owners, above the
|
|
6100
|
+
`Executing this recovery would transiently hold ${peak} owners, above the ${MAX_OWNERS}-owner maximum \u2014 new owners are added before old ones are removed.`,
|
|
5864
6101
|
{
|
|
5865
6102
|
remediation: "Run a recovery that retains at least one existing owner, then a second recovery to replace it."
|
|
5866
6103
|
}
|
|
@@ -5875,8 +6112,7 @@ var RecoveryModule = class {
|
|
|
5875
6112
|
async cancel(recoveryHash) {
|
|
5876
6113
|
const hash = normalizeTxHash(recoveryHash, "recovery hash");
|
|
5877
6114
|
const caller = await this.ctx.connection.address();
|
|
5878
|
-
const
|
|
5879
|
-
const isOwner = await vault2.getFunction("isOwner(address)")(caller);
|
|
6115
|
+
const isOwner = await this.vaultContract().isOwner(caller);
|
|
5880
6116
|
if (!isOwner) {
|
|
5881
6117
|
throw new PreconditionError(
|
|
5882
6118
|
`Only a current owner of the vault can cancel a recovery. ${caller} is not one.`
|
|
@@ -5893,7 +6129,7 @@ var RecoveryModule = class {
|
|
|
5893
6129
|
// Affordances
|
|
5894
6130
|
// =========================================================================
|
|
5895
6131
|
/** What `caller` may do to a recovery right now, and when blocked actions unlock. */
|
|
5896
|
-
async affordances(recoveryHash, caller) {
|
|
6132
|
+
async affordances(recoveryHash, caller, at) {
|
|
5897
6133
|
const hash = normalizeTxHash(recoveryHash, "recovery hash");
|
|
5898
6134
|
const who = caller ?? await this.ctx.connection.addressOrNull();
|
|
5899
6135
|
if (!who) {
|
|
@@ -5907,9 +6143,9 @@ var RecoveryModule = class {
|
|
|
5907
6143
|
this.isGuardian(address2),
|
|
5908
6144
|
this.contract().recoveryApprovals(this.vault, hash, address2),
|
|
5909
6145
|
this.isEnabled(),
|
|
5910
|
-
this.
|
|
6146
|
+
this.vaultContract().isOwner(address2)
|
|
5911
6147
|
]);
|
|
5912
|
-
const now =
|
|
6148
|
+
const now = at ?? this.now();
|
|
5913
6149
|
const terminal = request.status === "executed" || request.status === "cancelled";
|
|
5914
6150
|
const expired = now > request.expiration && request.expiration > 0;
|
|
5915
6151
|
const quorum = request.approvalCount >= request.requiredThreshold;
|
|
@@ -6006,13 +6242,10 @@ var RecoveryModule = class {
|
|
|
6006
6242
|
const approvalCount = Number(raw.approvalCount ?? 0);
|
|
6007
6243
|
const requiredThreshold = Number(raw.requiredThreshold ?? 0);
|
|
6008
6244
|
const executed = Boolean(raw.executed);
|
|
6009
|
-
const status = deriveRecoveryStatus(
|
|
6010
|
-
executed,
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
executionTime,
|
|
6014
|
-
expiration
|
|
6015
|
-
});
|
|
6245
|
+
const status = deriveRecoveryStatus(
|
|
6246
|
+
{ executed, approvalCount, requiredThreshold, executionTime, expiration },
|
|
6247
|
+
this.now()
|
|
6248
|
+
);
|
|
6016
6249
|
return {
|
|
6017
6250
|
hash,
|
|
6018
6251
|
vault: this.vault,
|
|
@@ -6032,7 +6265,7 @@ var RecoveryModule = class {
|
|
|
6032
6265
|
const seen = /* @__PURE__ */ new Set();
|
|
6033
6266
|
for (const owner of newOwners) {
|
|
6034
6267
|
const key = owner.toLowerCase();
|
|
6035
|
-
if (key ===
|
|
6268
|
+
if (key === ZERO_ADDRESS || key === SENTINEL_MODULES) {
|
|
6036
6269
|
throw new ValidationError(
|
|
6037
6270
|
`New owner ${owner} is reserved \u2014 the zero address and the module sentinel 0x\u202601 are rejected.`
|
|
6038
6271
|
);
|
|
@@ -6053,7 +6286,7 @@ var RecoveryModule = class {
|
|
|
6053
6286
|
if (request.executed) {
|
|
6054
6287
|
throw new PreconditionError("This recovery has already been executed.");
|
|
6055
6288
|
}
|
|
6056
|
-
if (request.expiration > 0 &&
|
|
6289
|
+
if (request.expiration > 0 && this.now() > request.expiration) {
|
|
6057
6290
|
throw new PreconditionError("This recovery has expired.", {
|
|
6058
6291
|
remediation: "Call expire() to clean it up, then initiate a new recovery."
|
|
6059
6292
|
});
|
|
@@ -6114,15 +6347,60 @@ function terminalReason(request) {
|
|
|
6114
6347
|
|
|
6115
6348
|
// src/balances.ts
|
|
6116
6349
|
var import_quais10 = require("quais");
|
|
6350
|
+
|
|
6351
|
+
// src/chain/token-contract.ts
|
|
6352
|
+
var TOKEN_RETRY = { maxAttempts: 2, baseDelayMs: 150 };
|
|
6353
|
+
var TokenContract = class {
|
|
6354
|
+
constructor(contract, retry = {}) {
|
|
6355
|
+
this.contract = contract;
|
|
6356
|
+
this.retry = { ...TOKEN_RETRY, ...retry };
|
|
6357
|
+
}
|
|
6358
|
+
contract;
|
|
6359
|
+
retry;
|
|
6360
|
+
read(signature, ...args) {
|
|
6361
|
+
return withRetry(() => this.contract.getFunction(signature)(...args), this.retry);
|
|
6362
|
+
}
|
|
6363
|
+
/** ERC20 units held, or — for ERC721 — the number of tokens held. */
|
|
6364
|
+
balanceOf(owner) {
|
|
6365
|
+
return this.read("balanceOf(address)", owner);
|
|
6366
|
+
}
|
|
6367
|
+
/** ERC721 only. Reverts for a burned or never-minted id. */
|
|
6368
|
+
ownerOf(tokenId) {
|
|
6369
|
+
return this.read("ownerOf(uint256)", tokenId);
|
|
6370
|
+
}
|
|
6371
|
+
/** ERC1155 only. One call for many ids, so it stays a single round trip. */
|
|
6372
|
+
balanceOfBatch(owners, ids) {
|
|
6373
|
+
return this.read("balanceOfBatch(address[],uint256[])", owners, ids);
|
|
6374
|
+
}
|
|
6375
|
+
};
|
|
6376
|
+
|
|
6377
|
+
// src/pool.ts
|
|
6378
|
+
async function mapPooled(items, limit, fn) {
|
|
6379
|
+
const results = new Array(items.length);
|
|
6380
|
+
let next = 0;
|
|
6381
|
+
const worker = async () => {
|
|
6382
|
+
for (; ; ) {
|
|
6383
|
+
const i = next++;
|
|
6384
|
+
if (i >= items.length) return;
|
|
6385
|
+
results[i] = await fn(items[i], i);
|
|
6386
|
+
}
|
|
6387
|
+
};
|
|
6388
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
6389
|
+
return results;
|
|
6390
|
+
}
|
|
6391
|
+
var DEFAULT_CONCURRENCY = 8;
|
|
6392
|
+
|
|
6393
|
+
// src/balances.ts
|
|
6117
6394
|
async function loadBalances(connection, queries, vault2, options = {}) {
|
|
6118
6395
|
const verify = options.verify !== false;
|
|
6119
6396
|
const maxTokens = options.maxTokens ?? 50;
|
|
6120
6397
|
const transferScanLimit = options.transferScanLimit ?? 500;
|
|
6121
6398
|
const maxTokenIdChecks = options.maxTokenIdChecks ?? 100;
|
|
6399
|
+
const concurrency = Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY);
|
|
6122
6400
|
const address2 = (0, import_quais10.getAddress)(vault2);
|
|
6123
6401
|
const native = BigInt(await connection.provider.getBalance(address2));
|
|
6124
6402
|
if (!queries) return { native, tokens: [] };
|
|
6125
|
-
const transfers = await queries.
|
|
6403
|
+
const transfers = await queries.tokenTransferScan(address2, transferScanLimit);
|
|
6126
6404
|
const seen = /* @__PURE__ */ new Map();
|
|
6127
6405
|
for (const row of transfers.data) {
|
|
6128
6406
|
const key = row.token_address.toLowerCase();
|
|
@@ -6141,17 +6419,18 @@ async function loadBalances(connection, queries, vault2, options = {}) {
|
|
|
6141
6419
|
}
|
|
6142
6420
|
const metadata = await queries.tokens(candidates);
|
|
6143
6421
|
const byAddress = new Map(metadata.map((t) => [t.address.toLowerCase(), t]));
|
|
6144
|
-
const balances = await
|
|
6145
|
-
candidates
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6422
|
+
const balances = await mapPooled(
|
|
6423
|
+
candidates,
|
|
6424
|
+
concurrency,
|
|
6425
|
+
(token2) => buildBalance(
|
|
6426
|
+
connection,
|
|
6427
|
+
address2,
|
|
6428
|
+
token2,
|
|
6429
|
+
byAddress.get(token2),
|
|
6430
|
+
seen.get(token2) ?? [],
|
|
6431
|
+
verify,
|
|
6432
|
+
maxTokenIdChecks,
|
|
6433
|
+
concurrency
|
|
6155
6434
|
)
|
|
6156
6435
|
);
|
|
6157
6436
|
return {
|
|
@@ -6161,30 +6440,35 @@ async function loadBalances(connection, queries, vault2, options = {}) {
|
|
|
6161
6440
|
...Object.keys(truncated).length ? { truncated } : {}
|
|
6162
6441
|
};
|
|
6163
6442
|
}
|
|
6164
|
-
async function buildBalance(connection, vault2, token2, meta, transfers, verify, maxTokenIdChecks) {
|
|
6443
|
+
async function buildBalance(connection, vault2, token2, meta, transfers, verify, maxTokenIdChecks, concurrency) {
|
|
6165
6444
|
const standard = meta?.standard ?? inferStandard(transfers);
|
|
6166
6445
|
const replayed = replayBalance(transfers, standard);
|
|
6167
6446
|
const base = {
|
|
6168
6447
|
token: (0, import_quais10.getAddress)(token2),
|
|
6169
6448
|
standard,
|
|
6170
|
-
|
|
6171
|
-
|
|
6449
|
+
// Deployer-controlled strings headed for someone's terminal. See `src/text.ts`.
|
|
6450
|
+
symbol: sanitizeText(meta?.symbol, 32) || "???",
|
|
6451
|
+
name: sanitizeText(meta?.name, 64) || "Unknown token",
|
|
6172
6452
|
decimals: meta?.decimals ?? (standard === "ERC20" ? 18 : 0),
|
|
6173
6453
|
balance: replayed.balance,
|
|
6174
6454
|
...replayed.tokenIds.length > 0 ? { tokenIds: replayed.tokenIds } : {},
|
|
6175
6455
|
verified: false
|
|
6176
6456
|
};
|
|
6177
6457
|
if (!verify) return base;
|
|
6458
|
+
const contract = new TokenContract(connection.token(token2, standard), connection.retry);
|
|
6178
6459
|
try {
|
|
6179
6460
|
if (standard === "ERC20") {
|
|
6180
|
-
|
|
6181
|
-
const balance = await contract2.getFunction("balanceOf(address)")(vault2);
|
|
6182
|
-
return { ...base, balance: BigInt(balance), verified: true };
|
|
6461
|
+
return { ...base, balance: BigInt(await contract.balanceOf(vault2)), verified: true };
|
|
6183
6462
|
}
|
|
6184
6463
|
if (standard === "ERC721") {
|
|
6185
|
-
const
|
|
6186
|
-
const
|
|
6187
|
-
|
|
6464
|
+
const count = await contract.balanceOf(vault2);
|
|
6465
|
+
const owned = await filterOwned(
|
|
6466
|
+
contract,
|
|
6467
|
+
vault2,
|
|
6468
|
+
replayed.tokenIds,
|
|
6469
|
+
maxTokenIdChecks,
|
|
6470
|
+
concurrency
|
|
6471
|
+
);
|
|
6188
6472
|
return {
|
|
6189
6473
|
...base,
|
|
6190
6474
|
balance: BigInt(count),
|
|
@@ -6193,10 +6477,9 @@ async function buildBalance(connection, vault2, token2, meta, transfers, verify,
|
|
|
6193
6477
|
verified: true
|
|
6194
6478
|
};
|
|
6195
6479
|
}
|
|
6196
|
-
const contract = new import_quais10.Contract(token2, Erc1155Abi, connection.provider);
|
|
6197
6480
|
const ids = replayed.tokenIds;
|
|
6198
6481
|
if (ids.length === 0) return { ...base, verified: true };
|
|
6199
|
-
const amounts = await contract.
|
|
6482
|
+
const amounts = await contract.balanceOfBatch(
|
|
6200
6483
|
ids.map(() => vault2),
|
|
6201
6484
|
ids
|
|
6202
6485
|
);
|
|
@@ -6241,18 +6524,16 @@ function replayBalance(transfers, standard) {
|
|
|
6241
6524
|
}
|
|
6242
6525
|
return { balance, tokenIds };
|
|
6243
6526
|
}
|
|
6244
|
-
async function filterOwned(contract, vault2, ids, limit) {
|
|
6527
|
+
async function filterOwned(contract, vault2, ids, limit, concurrency) {
|
|
6245
6528
|
if (ids.length === 0) return [];
|
|
6246
|
-
const results = await
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
})
|
|
6255
|
-
);
|
|
6529
|
+
const results = await mapPooled(ids.slice(0, limit), concurrency, async (id) => {
|
|
6530
|
+
try {
|
|
6531
|
+
const owner = await contract.ownerOf(id);
|
|
6532
|
+
return owner.toLowerCase() === vault2.toLowerCase() ? id : null;
|
|
6533
|
+
} catch {
|
|
6534
|
+
return null;
|
|
6535
|
+
}
|
|
6536
|
+
});
|
|
6256
6537
|
return results.filter((id) => id !== null);
|
|
6257
6538
|
}
|
|
6258
6539
|
function inferStandard(transfers) {
|
|
@@ -6288,7 +6569,7 @@ function watchVault(client, vault2, handler, options = {}) {
|
|
|
6288
6569
|
const topics = options.topics ?? DEFAULT_TOPICS;
|
|
6289
6570
|
const address2 = vault2.toLowerCase();
|
|
6290
6571
|
const schema = client.config.schema;
|
|
6291
|
-
const channel = client.
|
|
6572
|
+
const channel = client.channel(`quaivault:${schema}:${address2}`);
|
|
6292
6573
|
for (const topic of topics) {
|
|
6293
6574
|
const table = TABLE_FOR[topic];
|
|
6294
6575
|
channel.on(
|
|
@@ -6318,7 +6599,7 @@ function watchVault(client, vault2, handler, options = {}) {
|
|
|
6318
6599
|
return {
|
|
6319
6600
|
topics,
|
|
6320
6601
|
async unsubscribe() {
|
|
6321
|
-
await client.
|
|
6602
|
+
await client.removeChannel(channel);
|
|
6322
6603
|
}
|
|
6323
6604
|
};
|
|
6324
6605
|
}
|
|
@@ -6767,8 +7048,9 @@ function classifyExecution(receipt, vault2, txHash) {
|
|
|
6767
7048
|
}
|
|
6768
7049
|
const thresholdReached = eventFor(events, "ThresholdReached", txHash);
|
|
6769
7050
|
if (thresholdReached) {
|
|
7051
|
+
const approvedAt = toNumber2(thresholdReached.args.approvedAt);
|
|
6770
7052
|
const executableAfter = toNumber2(thresholdReached.args.executableAfter);
|
|
6771
|
-
if (executableAfter >
|
|
7053
|
+
if (executableAfter > approvedAt) {
|
|
6772
7054
|
return {
|
|
6773
7055
|
...base,
|
|
6774
7056
|
outcome: "timelock_started",
|
|
@@ -6798,7 +7080,7 @@ function extractProposedTxHash(receipt, vault2) {
|
|
|
6798
7080
|
}
|
|
6799
7081
|
|
|
6800
7082
|
// src/vault.ts
|
|
6801
|
-
var Vault = class {
|
|
7083
|
+
var Vault = class _Vault {
|
|
6802
7084
|
address;
|
|
6803
7085
|
/** Guardian-based recovery for this vault. */
|
|
6804
7086
|
recovery;
|
|
@@ -6810,7 +7092,8 @@ var Vault = class {
|
|
|
6810
7092
|
connection: ctx.connection,
|
|
6811
7093
|
queries: ctx.queries,
|
|
6812
7094
|
vaultAddress: this.address,
|
|
6813
|
-
moduleAddress: ctx.contracts.socialRecovery
|
|
7095
|
+
moduleAddress: ctx.contracts.socialRecovery,
|
|
7096
|
+
...ctx.now ? { now: ctx.now } : {}
|
|
6814
7097
|
});
|
|
6815
7098
|
}
|
|
6816
7099
|
// =========================================================================
|
|
@@ -6841,6 +7124,30 @@ var Vault = class {
|
|
|
6841
7124
|
if (!this.ctx.queries) throw new NoIndexerError(operation);
|
|
6842
7125
|
return this.ctx.queries;
|
|
6843
7126
|
}
|
|
7127
|
+
/**
|
|
7128
|
+
* The indexer's head, for stamping onto records read from it.
|
|
7129
|
+
*
|
|
7130
|
+
* Reads the cached health result rather than querying `indexer_state` directly.
|
|
7131
|
+
* `useIndexer()` has just called `health()` on this same code path and the result is
|
|
7132
|
+
* cached, so this is free — whereas a `state()` call is a second round trip per
|
|
7133
|
+
* hydration, paid on every indexed read purely to fill one advisory field.
|
|
7134
|
+
*
|
|
7135
|
+
* Undefined when the indexer is not answering: a head of 0 would read as "indexed at
|
|
7136
|
+
* genesis" rather than "unknown".
|
|
7137
|
+
*/
|
|
7138
|
+
async indexerHead() {
|
|
7139
|
+
const health = await this.ctx.indexer?.health();
|
|
7140
|
+
return health?.available ? health.lastIndexedBlock : void 0;
|
|
7141
|
+
}
|
|
7142
|
+
/**
|
|
7143
|
+
* Now, in Unix seconds, for anything compared against a chain timestamp.
|
|
7144
|
+
*
|
|
7145
|
+
* Never used to measure elapsed time — see the duration sites in
|
|
7146
|
+
* {@link waitForExecutable}, which stay on the raw local clock deliberately.
|
|
7147
|
+
*/
|
|
7148
|
+
now() {
|
|
7149
|
+
return (this.ctx.now ?? nowSeconds)();
|
|
7150
|
+
}
|
|
6844
7151
|
contract(write = false) {
|
|
6845
7152
|
return new VaultContract(this.ctx.connection.vault(this.address, write), this.ctx.connection.retry);
|
|
6846
7153
|
}
|
|
@@ -6868,8 +7175,48 @@ var Vault = class {
|
|
|
6868
7175
|
balance: BigInt(balance)
|
|
6869
7176
|
};
|
|
6870
7177
|
}
|
|
6871
|
-
/**
|
|
7178
|
+
/**
|
|
7179
|
+
* Capture owners and threshold once, for reuse across a burst of reads.
|
|
7180
|
+
*
|
|
7181
|
+
* Pair with {@link pinned}:
|
|
7182
|
+
*
|
|
7183
|
+
* ```ts
|
|
7184
|
+
* const view = await vault.view();
|
|
7185
|
+
* const pinned = vault.pinned(view);
|
|
7186
|
+
* // every read below shares one owner/threshold read
|
|
7187
|
+
* const txs = await pinned.transactions(hashes);
|
|
7188
|
+
* ```
|
|
7189
|
+
*/
|
|
7190
|
+
async view() {
|
|
7191
|
+
const fromIndexer = await this.useIndexer();
|
|
7192
|
+
const [owners, threshold, indexedAtBlock] = await Promise.all([
|
|
7193
|
+
this.owners(),
|
|
7194
|
+
this.threshold(),
|
|
7195
|
+
this.indexerHead()
|
|
7196
|
+
]);
|
|
7197
|
+
return {
|
|
7198
|
+
owners,
|
|
7199
|
+
threshold,
|
|
7200
|
+
...indexedAtBlock !== void 0 ? { indexedAtBlock } : {},
|
|
7201
|
+
capturedAt: this.now(),
|
|
7202
|
+
source: fromIndexer ? "indexer" : "chain"
|
|
7203
|
+
};
|
|
7204
|
+
}
|
|
7205
|
+
/**
|
|
7206
|
+
* A handle that answers {@link owners} and {@link threshold} from `view` instead of
|
|
7207
|
+
* re-reading them.
|
|
7208
|
+
*
|
|
7209
|
+
* Reads only. Write preconditions go through `chainOwners` / `chainThreshold`, which
|
|
7210
|
+
* bypass the snapshot by construction — pinning a view can never cause this SDK to
|
|
7211
|
+
* sign against a stale owner set. Everything else about the handle, including the
|
|
7212
|
+
* connection and the recovery module, is shared with the original.
|
|
7213
|
+
*/
|
|
7214
|
+
pinned(view) {
|
|
7215
|
+
return new _Vault(this.address, { ...this.ctx, view });
|
|
7216
|
+
}
|
|
7217
|
+
/** Active owners. Prefers a pinned view, then the indexer, then chain. */
|
|
6872
7218
|
async owners() {
|
|
7219
|
+
if (this.ctx.view) return this.ctx.view.owners;
|
|
6873
7220
|
if (await this.useIndexer()) {
|
|
6874
7221
|
try {
|
|
6875
7222
|
const owners = await this.requireQueries("owners").owners(this.address);
|
|
@@ -6877,12 +7224,55 @@ var Vault = class {
|
|
|
6877
7224
|
} catch {
|
|
6878
7225
|
}
|
|
6879
7226
|
}
|
|
7227
|
+
return this.chainOwners();
|
|
7228
|
+
}
|
|
7229
|
+
/**
|
|
7230
|
+
* Owners straight from the chain, bypassing the indexer entirely.
|
|
7231
|
+
*
|
|
7232
|
+
* {@link owners} prefers the indexer, which is right for display and wrong for
|
|
7233
|
+
* anything that gates a write. A lagging indexer would let the SDK build a proposal
|
|
7234
|
+
* against an owner set that no longer exists — admitting a `removeOwner` that drops
|
|
7235
|
+
* the vault below its threshold, or rejecting one that is perfectly valid. Every
|
|
7236
|
+
* propose-time precondition reads through here instead.
|
|
7237
|
+
*/
|
|
7238
|
+
async chainOwners() {
|
|
6880
7239
|
return (await this.contract().getOwners()).map((o) => (0, import_quais14.getAddress)(String(o)));
|
|
6881
7240
|
}
|
|
6882
7241
|
async isOwner(address2) {
|
|
6883
7242
|
return this.contract().isOwner((0, import_quais14.getAddress)(address2));
|
|
6884
7243
|
}
|
|
7244
|
+
/**
|
|
7245
|
+
* Whether `owner`'s approval on `txHash` currently counts toward the threshold.
|
|
7246
|
+
*
|
|
7247
|
+
* Reads the chain, and accounts for approval-epoch invalidation: removing an owner
|
|
7248
|
+
* bumps the vault's epoch and voids every approval that address made, so a
|
|
7249
|
+
* confirmation the indexer still lists may already be dead. This is the authoritative
|
|
7250
|
+
* answer.
|
|
7251
|
+
*
|
|
7252
|
+
* Public because it is one of the two inputs {@link computeAffordances} needs. Without
|
|
7253
|
+
* it a consumer wanting affordances at an adjusted time had no way to assemble an
|
|
7254
|
+
* `AffordanceContext` short of reimplementing this method against the raw contract.
|
|
7255
|
+
*/
|
|
7256
|
+
async hasApproved(txHash, owner) {
|
|
7257
|
+
return this.contract().hasApproved(normalizeTxHash(txHash), (0, import_quais14.getAddress)(owner));
|
|
7258
|
+
}
|
|
7259
|
+
/** Approvals required to execute. Prefers a pinned view, then the indexer, then chain. */
|
|
6885
7260
|
async threshold() {
|
|
7261
|
+
if (this.ctx.view) return this.ctx.view.threshold;
|
|
7262
|
+
if (await this.useIndexer()) {
|
|
7263
|
+
try {
|
|
7264
|
+
const wallet = await this.requireQueries("threshold").wallet(this.address);
|
|
7265
|
+
if (wallet) return wallet.threshold;
|
|
7266
|
+
} catch {
|
|
7267
|
+
}
|
|
7268
|
+
}
|
|
7269
|
+
return this.chainThreshold();
|
|
7270
|
+
}
|
|
7271
|
+
/**
|
|
7272
|
+
* Threshold straight from the chain. The write-path counterpart to {@link threshold},
|
|
7273
|
+
* for the same reason {@link chainOwners} exists.
|
|
7274
|
+
*/
|
|
7275
|
+
async chainThreshold() {
|
|
6886
7276
|
return Number(await this.contract().threshold());
|
|
6887
7277
|
}
|
|
6888
7278
|
async balance() {
|
|
@@ -6935,6 +7325,53 @@ var Vault = class {
|
|
|
6935
7325
|
}
|
|
6936
7326
|
return this.fromChain(hash);
|
|
6937
7327
|
}
|
|
7328
|
+
/**
|
|
7329
|
+
* Several transactions at once, keyed by hash.
|
|
7330
|
+
*
|
|
7331
|
+
* The plural form exists because the singular one is expensive to loop: each
|
|
7332
|
+
* `transaction()` re-reads the owner set, the threshold and the indexer head, so
|
|
7333
|
+
* fetching fifty costs fifty times that. This resolves the whole set the way the
|
|
7334
|
+
* listing methods already do — one query for the rows, one owner/threshold read
|
|
7335
|
+
* shared across them, one batched confirmations query.
|
|
7336
|
+
*
|
|
7337
|
+
* Hashes the indexer does not have fall back to chain reads, bounded by a pool.
|
|
7338
|
+
* Hashes that exist nowhere are simply absent from the map rather than throwing:
|
|
7339
|
+
* one unknown hash should not lose the caller the other forty-nine.
|
|
7340
|
+
*/
|
|
7341
|
+
async transactions(txHashes) {
|
|
7342
|
+
const hashes = [...new Set(txHashes.map((h) => normalizeTxHash(h)))];
|
|
7343
|
+
const found = /* @__PURE__ */ new Map();
|
|
7344
|
+
if (hashes.length === 0) return found;
|
|
7345
|
+
if (await this.useIndexer()) {
|
|
7346
|
+
try {
|
|
7347
|
+
const queries = this.requireQueries("Reading transactions");
|
|
7348
|
+
const [rows, owners, threshold] = await Promise.all([
|
|
7349
|
+
queries.transactionsByHash(this.address, hashes),
|
|
7350
|
+
this.owners(),
|
|
7351
|
+
this.threshold()
|
|
7352
|
+
]);
|
|
7353
|
+
for (const tx of await this.hydrateRows(rows, owners, threshold)) {
|
|
7354
|
+
found.set(tx.hash.toLowerCase(), tx);
|
|
7355
|
+
}
|
|
7356
|
+
} catch {
|
|
7357
|
+
}
|
|
7358
|
+
}
|
|
7359
|
+
const missing = hashes.filter((hash) => !found.has(hash));
|
|
7360
|
+
if (missing.length > 0) {
|
|
7361
|
+
const resolved = await mapPooled(missing, DEFAULT_CONCURRENCY, async (hash) => {
|
|
7362
|
+
try {
|
|
7363
|
+
return await this.fromChain(hash);
|
|
7364
|
+
} catch (err) {
|
|
7365
|
+
if (err instanceof NotFoundError) return null;
|
|
7366
|
+
throw err;
|
|
7367
|
+
}
|
|
7368
|
+
});
|
|
7369
|
+
for (const tx of resolved) {
|
|
7370
|
+
if (tx) found.set(tx.hash.toLowerCase(), tx);
|
|
7371
|
+
}
|
|
7372
|
+
}
|
|
7373
|
+
return found;
|
|
7374
|
+
}
|
|
6938
7375
|
async pendingTransactions(options = {}) {
|
|
6939
7376
|
const queries = this.requireQueries("Listing pending transactions");
|
|
6940
7377
|
const [rows, owners, threshold] = await Promise.all([
|
|
@@ -6965,7 +7402,7 @@ var Vault = class {
|
|
|
6965
7402
|
this.address,
|
|
6966
7403
|
rows.map((r) => r.tx_hash)
|
|
6967
7404
|
);
|
|
6968
|
-
const indexedAtBlock =
|
|
7405
|
+
const indexedAtBlock = await this.indexerHead();
|
|
6969
7406
|
return rows.map((row) => {
|
|
6970
7407
|
const confirmed = (confirmations.get(row.tx_hash.toLowerCase()) ?? []).map(
|
|
6971
7408
|
(c) => c.owner_address
|
|
@@ -6988,7 +7425,7 @@ var Vault = class {
|
|
|
6988
7425
|
]);
|
|
6989
7426
|
if (!row) return null;
|
|
6990
7427
|
const confirmations = await queries.confirmations(this.address, hash);
|
|
6991
|
-
const indexedAtBlock =
|
|
7428
|
+
const indexedAtBlock = await this.indexerHead();
|
|
6992
7429
|
return this.buildTransaction({
|
|
6993
7430
|
row,
|
|
6994
7431
|
confirmedBy: confirmations.filter((c) => c.is_active).map((c) => c.owner_address),
|
|
@@ -7042,7 +7479,7 @@ var Vault = class {
|
|
|
7042
7479
|
approvalCount,
|
|
7043
7480
|
threshold,
|
|
7044
7481
|
failed: row.status === "failed"
|
|
7045
|
-
});
|
|
7482
|
+
}, this.now());
|
|
7046
7483
|
const failedReturnData = row.failed_return_data ?? void 0;
|
|
7047
7484
|
return {
|
|
7048
7485
|
hash: row.tx_hash,
|
|
@@ -7051,7 +7488,10 @@ var Vault = class {
|
|
|
7051
7488
|
value,
|
|
7052
7489
|
data,
|
|
7053
7490
|
proposer: (0, import_quais14.getAddress)(row.submitted_by),
|
|
7054
|
-
|
|
7491
|
+
// The indexer records the block, not the chain timestamp — they are not
|
|
7492
|
+
// interchangeable, and reporting a block number as `proposedAt` renders as 1970.
|
|
7493
|
+
proposedAt: 0,
|
|
7494
|
+
proposedAtBlock: toNumber(row.submitted_at_block),
|
|
7055
7495
|
kind: decodeResult.kind,
|
|
7056
7496
|
decoded: decodeResult.decoded,
|
|
7057
7497
|
summary: decodeResult.summary,
|
|
@@ -7122,7 +7562,7 @@ var Vault = class {
|
|
|
7122
7562
|
approvedAt,
|
|
7123
7563
|
approvalCount: approvals.length,
|
|
7124
7564
|
threshold: Number(threshold)
|
|
7125
|
-
}),
|
|
7565
|
+
}, this.now()),
|
|
7126
7566
|
approvals,
|
|
7127
7567
|
approvalCount: approvals.length,
|
|
7128
7568
|
threshold: Number(threshold),
|
|
@@ -7140,8 +7580,20 @@ var Vault = class {
|
|
|
7140
7580
|
* What `caller` may legally do to `txHash` right now, and when blocked actions
|
|
7141
7581
|
* become available. Defaults to the connected signer's address.
|
|
7142
7582
|
*/
|
|
7143
|
-
async affordances(txHash, caller) {
|
|
7583
|
+
async affordances(txHash, caller, at) {
|
|
7144
7584
|
const hash = normalizeTxHash(txHash);
|
|
7585
|
+
return this.resolveAffordances(hash, this.transaction(hash), caller, at);
|
|
7586
|
+
}
|
|
7587
|
+
/**
|
|
7588
|
+
* Shared body of {@link affordances}, taking the transaction either as a value or as
|
|
7589
|
+
* an in-flight promise.
|
|
7590
|
+
*
|
|
7591
|
+
* The promise form is the point: a caller that already holds the record (like
|
|
7592
|
+
* {@link describe}) passes it and skips a redundant fetch, while `affordances()`
|
|
7593
|
+
* passes the unawaited promise so the transaction read still overlaps the two
|
|
7594
|
+
* ownership probes rather than serialising behind them.
|
|
7595
|
+
*/
|
|
7596
|
+
async resolveAffordances(hash, transaction, caller, at) {
|
|
7145
7597
|
const who = caller ?? await this.ctx.connection.addressOrNull();
|
|
7146
7598
|
if (!who) {
|
|
7147
7599
|
throw new ValidationError(
|
|
@@ -7150,11 +7602,17 @@ var Vault = class {
|
|
|
7150
7602
|
}
|
|
7151
7603
|
const vault2 = this.contract();
|
|
7152
7604
|
const [tx, isOwner, hasApproved] = await Promise.all([
|
|
7153
|
-
|
|
7605
|
+
transaction,
|
|
7154
7606
|
vault2.isOwner((0, import_quais14.getAddress)(who)),
|
|
7155
7607
|
vault2.hasApproved(hash, (0, import_quais14.getAddress)(who))
|
|
7156
7608
|
]);
|
|
7157
|
-
return computeAffordances({
|
|
7609
|
+
return computeAffordances({
|
|
7610
|
+
tx,
|
|
7611
|
+
caller: (0, import_quais14.getAddress)(who),
|
|
7612
|
+
isOwner,
|
|
7613
|
+
hasApproved,
|
|
7614
|
+
at: at ?? this.now()
|
|
7615
|
+
});
|
|
7158
7616
|
}
|
|
7159
7617
|
// =========================================================================
|
|
7160
7618
|
// Proposals
|
|
@@ -7245,7 +7703,7 @@ var Vault = class {
|
|
|
7245
7703
|
// --- self-calls -------------------------------------------------------
|
|
7246
7704
|
addOwner: async (owner, options = {}) => this.proposeSelfCall(selfCall.addOwner(assertQuaiAddress(owner, "owner")), options),
|
|
7247
7705
|
removeOwner: async (owner, options = {}) => {
|
|
7248
|
-
const [owners, threshold] = await Promise.all([this.
|
|
7706
|
+
const [owners, threshold] = await Promise.all([this.chainOwners(), this.chainThreshold()]);
|
|
7249
7707
|
if (!owners.some((o) => o.toLowerCase() === owner.toLowerCase())) {
|
|
7250
7708
|
throw new PreconditionError(`${owner} is not an owner of this vault.`);
|
|
7251
7709
|
}
|
|
@@ -7258,7 +7716,7 @@ var Vault = class {
|
|
|
7258
7716
|
return this.proposeSelfCall(selfCall.removeOwner((0, import_quais14.getAddress)(owner)), options);
|
|
7259
7717
|
},
|
|
7260
7718
|
changeThreshold: async (threshold, options = {}) => {
|
|
7261
|
-
const owners = await this.
|
|
7719
|
+
const owners = await this.chainOwners();
|
|
7262
7720
|
if (threshold < 1 || threshold > owners.length) {
|
|
7263
7721
|
throw new ValidationError(
|
|
7264
7722
|
`Invalid threshold ${threshold}: this vault has ${owners.length} owners.`
|
|
@@ -7365,11 +7823,11 @@ var Vault = class {
|
|
|
7365
7823
|
}
|
|
7366
7824
|
if (expiration > 0) {
|
|
7367
7825
|
const floor = isSelfCall ? 0 : Math.max(executionDelay, await this.minExecutionDelay());
|
|
7368
|
-
const earliest = minimumExpiration(floor, 0);
|
|
7826
|
+
const earliest = minimumExpiration(floor, 0, this.now());
|
|
7369
7827
|
if (expiration <= earliest) {
|
|
7370
7828
|
throw new ValidationError(
|
|
7371
7829
|
`expiration ${expiration} is too soon: with an effective delay of ${floor}s the vault requires an expiration after ${earliest}.`,
|
|
7372
|
-
`Use at least ${minimumExpiration(floor)} to leave a margin for block time.`
|
|
7830
|
+
`Use at least ${minimumExpiration(floor, 300, this.now())} to leave a margin for block time.`
|
|
7373
7831
|
);
|
|
7374
7832
|
}
|
|
7375
7833
|
}
|
|
@@ -7692,9 +8150,7 @@ var Vault = class {
|
|
|
7692
8150
|
const pollIntervalMs = options.pollIntervalMs ?? 15e3;
|
|
7693
8151
|
const deadline = Date.now() + timeoutMs;
|
|
7694
8152
|
for (; ; ) {
|
|
7695
|
-
if (options.signal?.aborted)
|
|
7696
|
-
throw new PreconditionError("Waiting for the transaction was aborted.");
|
|
7697
|
-
}
|
|
8153
|
+
if (options.signal?.aborted) throw new AbortError("Waiting for the transaction");
|
|
7698
8154
|
const tx = await this.fromChain(hash);
|
|
7699
8155
|
options.onPoll?.(tx);
|
|
7700
8156
|
if (tx.status === "ready") return tx;
|
|
@@ -7710,7 +8166,7 @@ var Vault = class {
|
|
|
7710
8166
|
{ remediation: "Collect the remaining approvals, then wait for the timelock." }
|
|
7711
8167
|
);
|
|
7712
8168
|
}
|
|
7713
|
-
const untilExecutable = tx.executableAfter > 0 ? tx.executableAfter
|
|
8169
|
+
const untilExecutable = tx.executableAfter > 0 ? (tx.executableAfter - this.now()) * 1e3 : pollIntervalMs;
|
|
7714
8170
|
const wait = Math.max(1e3, Math.min(untilExecutable, pollIntervalMs));
|
|
7715
8171
|
if (Date.now() + wait > deadline) {
|
|
7716
8172
|
throw new PreconditionError(
|
|
@@ -7740,7 +8196,7 @@ var Vault = class {
|
|
|
7740
8196
|
lines.push(` quorum reached: ${new Date(tx.approvedAt * 1e3).toISOString()}`);
|
|
7741
8197
|
}
|
|
7742
8198
|
if (tx.executableAfter > 0) {
|
|
7743
|
-
const remaining = tx.executableAfter -
|
|
8199
|
+
const remaining = tx.executableAfter - this.now();
|
|
7744
8200
|
lines.push(
|
|
7745
8201
|
` executable after: ${new Date(tx.executableAfter * 1e3).toISOString()}` + (remaining > 0 ? ` (in ${remaining}s)` : " (now)")
|
|
7746
8202
|
);
|
|
@@ -7751,7 +8207,7 @@ var Vault = class {
|
|
|
7751
8207
|
if (tx.decodedRevert) lines.push(` revert: ${tx.decodedRevert.message}`);
|
|
7752
8208
|
lines.push(` source: ${tx.source}`);
|
|
7753
8209
|
if (caller || this.ctx.connection.hasSigner()) {
|
|
7754
|
-
const affordances = await this.
|
|
8210
|
+
const affordances = await this.resolveAffordances(tx.hash, tx, caller);
|
|
7755
8211
|
const allowed = affordances.filter((a) => a.allowed).map((a) => a.action);
|
|
7756
8212
|
lines.push(` you can: ${allowed.length > 0 ? allowed.join(", ") : "nothing right now"}`);
|
|
7757
8213
|
}
|
|
@@ -7785,6 +8241,11 @@ function toRevertError(err, context) {
|
|
|
7785
8241
|
|
|
7786
8242
|
// src/client.ts
|
|
7787
8243
|
var QuaiVaultClient = class {
|
|
8244
|
+
/**
|
|
8245
|
+
* Source of "now" for time compared against chain timestamps. Resolved once from
|
|
8246
|
+
* {@link ClientOptions.now}, defaulting to the local clock. See {@link Clock}.
|
|
8247
|
+
*/
|
|
8248
|
+
now;
|
|
7788
8249
|
/**
|
|
7789
8250
|
* Resolved configuration, with the private key stripped and the indexer key
|
|
7790
8251
|
* masked — safe to log. The key itself is consumed once when building the signer.
|
|
@@ -7797,6 +8258,7 @@ var QuaiVaultClient = class {
|
|
|
7797
8258
|
constructor(options = {}) {
|
|
7798
8259
|
const resolved = resolveConfig(options);
|
|
7799
8260
|
this.config = redactConfig(resolved);
|
|
8261
|
+
this.now = resolved.now;
|
|
7800
8262
|
this.connection = new Connection(resolved, {
|
|
7801
8263
|
...options.provider ? { provider: options.provider } : {},
|
|
7802
8264
|
...options.signer ? { signer: options.signer } : {}
|
|
@@ -7832,7 +8294,8 @@ var QuaiVaultClient = class {
|
|
|
7832
8294
|
queries: this.queries,
|
|
7833
8295
|
contracts: this.config.contracts,
|
|
7834
8296
|
consistency: this.config.consistency,
|
|
7835
|
-
maxIndexerLagBlocks: this.config.maxIndexerLagBlocks
|
|
8297
|
+
maxIndexerLagBlocks: this.config.maxIndexerLagBlocks,
|
|
8298
|
+
now: this.now
|
|
7836
8299
|
};
|
|
7837
8300
|
}
|
|
7838
8301
|
/** Vault discovery. Requires the indexer — there is no on-chain reverse index. */
|