@quaivault/sdk 0.1.0 → 0.2.0
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/index.cjs +614 -199
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +346 -28
- package/dist/index.d.ts +346 -28
- package/dist/index.js +616 -201
- 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}`;
|
|
@@ -3818,7 +3841,9 @@ function resolveConfig(options = {}) {
|
|
|
3818
3841
|
contracts,
|
|
3819
3842
|
indexer,
|
|
3820
3843
|
rpcUrl,
|
|
3821
|
-
|
|
3844
|
+
// An explicit signer wins outright in `Connection`, so resolving a key here would
|
|
3845
|
+
// only pull a secret into memory that nothing can use. Skip it.
|
|
3846
|
+
privateKey: options.signer ? void 0 : pick(options.privateKey, env[ENV_VARS.privateKey]),
|
|
3822
3847
|
consistency,
|
|
3823
3848
|
maxIndexerLagBlocks: options.maxIndexerLagBlocks ?? parseLag(env[ENV_VARS.maxIndexerLagBlocks]) ?? 50,
|
|
3824
3849
|
retry: {
|
|
@@ -3948,7 +3973,7 @@ function isTransient(error) {
|
|
|
3948
3973
|
}
|
|
3949
3974
|
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
3950
3975
|
if (signal?.aborted) {
|
|
3951
|
-
reject(new
|
|
3976
|
+
reject(new AbortError("Waiting to retry"));
|
|
3952
3977
|
return;
|
|
3953
3978
|
}
|
|
3954
3979
|
const timer = setTimeout(() => {
|
|
@@ -3957,7 +3982,7 @@ var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
3957
3982
|
}, ms);
|
|
3958
3983
|
const onAbort = () => {
|
|
3959
3984
|
clearTimeout(timer);
|
|
3960
|
-
reject(new
|
|
3985
|
+
reject(new AbortError("Waiting to retry"));
|
|
3961
3986
|
};
|
|
3962
3987
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3963
3988
|
});
|
|
@@ -3967,7 +3992,7 @@ async function withRetry(fn, options = {}) {
|
|
|
3967
3992
|
const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;
|
|
3968
3993
|
let lastError;
|
|
3969
3994
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
3970
|
-
if (options.signal?.aborted) throw new
|
|
3995
|
+
if (options.signal?.aborted) throw new AbortError("The retried operation");
|
|
3971
3996
|
try {
|
|
3972
3997
|
return await fn();
|
|
3973
3998
|
} catch (error) {
|
|
@@ -4162,8 +4187,39 @@ var FactoryContract = class {
|
|
|
4162
4187
|
|
|
4163
4188
|
// src/errors/decode.ts
|
|
4164
4189
|
var import_quais4 = require("quais");
|
|
4190
|
+
|
|
4191
|
+
// src/text.ts
|
|
4192
|
+
var UNSAFE_CHARS = new RegExp(
|
|
4193
|
+
[
|
|
4194
|
+
"[\\u0000-\\u001F",
|
|
4195
|
+
// C0 controls, incl. ESC (0x1B) — the head of every ANSI sequence
|
|
4196
|
+
"\\u007F-\\u009F",
|
|
4197
|
+
// DEL and the C1 controls
|
|
4198
|
+
"\\u200B-\\u200F",
|
|
4199
|
+
// zero-width space/joiners, LRM, RLM
|
|
4200
|
+
"\\u202A-\\u202E",
|
|
4201
|
+
// bidi embeddings and overrides
|
|
4202
|
+
"\\u2060-\\u2064",
|
|
4203
|
+
// word joiner and the invisible operators
|
|
4204
|
+
"\\u2066-\\u2069",
|
|
4205
|
+
// bidi isolates
|
|
4206
|
+
"\\uFEFF]"
|
|
4207
|
+
// zero-width no-break space / BOM
|
|
4208
|
+
].join(""),
|
|
4209
|
+
"g"
|
|
4210
|
+
);
|
|
4211
|
+
var DEFAULT_TEXT_LIMIT = 128;
|
|
4212
|
+
function sanitizeText(value, maxLength = DEFAULT_TEXT_LIMIT) {
|
|
4213
|
+
if (typeof value !== "string") return "";
|
|
4214
|
+
const cleaned = value.replace(UNSAFE_CHARS, "").trim();
|
|
4215
|
+
if (cleaned.length <= maxLength) return cleaned;
|
|
4216
|
+
return `${cleaned.slice(0, Math.max(0, maxLength - 1))}\u2026`;
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
// src/errors/decode.ts
|
|
4165
4220
|
var SOLIDITY_ERROR_SELECTOR = "0x08c379a0";
|
|
4166
4221
|
var SOLIDITY_PANIC_SELECTOR = "0x4e487b71";
|
|
4222
|
+
var REVERT_TEXT_LIMIT = 256;
|
|
4167
4223
|
var registry = null;
|
|
4168
4224
|
function buildRegistry() {
|
|
4169
4225
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -4253,7 +4309,7 @@ function formatArgs(fragment, args) {
|
|
|
4253
4309
|
if (args.length === 0) return "";
|
|
4254
4310
|
const parts = fragment.inputs.map((input, i) => {
|
|
4255
4311
|
const value = args[i];
|
|
4256
|
-
const rendered = typeof value === "bigint" ? value.toString() : String(value);
|
|
4312
|
+
const rendered = typeof value === "bigint" ? value.toString() : typeof value === "string" ? sanitizeText(value, REVERT_TEXT_LIMIT) : String(value);
|
|
4257
4313
|
return input.name ? `${input.name}: ${rendered}` : rendered;
|
|
4258
4314
|
});
|
|
4259
4315
|
return `(${parts.join(", ")})`;
|
|
@@ -4269,7 +4325,7 @@ function decodeRevert(data) {
|
|
|
4269
4325
|
name: "Error",
|
|
4270
4326
|
args: [reason],
|
|
4271
4327
|
selector,
|
|
4272
|
-
message:
|
|
4328
|
+
message: sanitizeText(reason, REVERT_TEXT_LIMIT)
|
|
4273
4329
|
};
|
|
4274
4330
|
} catch {
|
|
4275
4331
|
return void 0;
|
|
@@ -4357,6 +4413,7 @@ var MAX_EXECUTION_DELAY = 30 * 24 * 60 * 60;
|
|
|
4357
4413
|
var MAX_OWNERS = 20;
|
|
4358
4414
|
var MAX_MODULES = 50;
|
|
4359
4415
|
var SENTINEL_MODULES = "0x0000000000000000000000000000000000000001";
|
|
4416
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
4360
4417
|
var selfCall = {
|
|
4361
4418
|
addOwner: (owner) => vault.encodeFunctionData("addOwner", [owner]),
|
|
4362
4419
|
removeOwner: (owner) => vault.encodeFunctionData("removeOwner", [owner]),
|
|
@@ -4524,7 +4581,7 @@ var syncStrategy = {
|
|
|
4524
4581
|
async mine({ factory, deployer, bytecodeHash, targetPrefix, maxAttempts, timeoutMs, onProgress, signal }) {
|
|
4525
4582
|
const startedAt = Date.now();
|
|
4526
4583
|
for (let attempts = 0; attempts < maxAttempts; ) {
|
|
4527
|
-
if (signal?.aborted) throw new
|
|
4584
|
+
if (signal?.aborted) throw new AbortError("Salt mining");
|
|
4528
4585
|
if (Date.now() - startedAt > timeoutMs) {
|
|
4529
4586
|
throw new SaltMiningError(
|
|
4530
4587
|
`Salt mining timed out after ${timeoutMs} ms (${attempts} attempts).`,
|
|
@@ -4552,109 +4609,127 @@ var syncStrategy = {
|
|
|
4552
4609
|
);
|
|
4553
4610
|
}
|
|
4554
4611
|
};
|
|
4555
|
-
var
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4612
|
+
var WorkersUnavailable = class extends Error {
|
|
4613
|
+
};
|
|
4614
|
+
async function loadNodeWorkers() {
|
|
4615
|
+
try {
|
|
4616
|
+
const { Worker } = await import("worker_threads");
|
|
4617
|
+
const os = await import("os");
|
|
4618
|
+
return { Worker, parallelism: (os.availableParallelism ?? (() => os.cpus().length))() };
|
|
4619
|
+
} catch {
|
|
4620
|
+
return null;
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
function createWorkerThreadsStrategy(load = loadNodeWorkers) {
|
|
4624
|
+
return {
|
|
4625
|
+
name: "worker_threads",
|
|
4626
|
+
async mine(job) {
|
|
4627
|
+
const runtime = await load();
|
|
4628
|
+
if (!runtime) return syncStrategy.mine(job);
|
|
4629
|
+
try {
|
|
4630
|
+
return await mineInWorkers(runtime.Worker, runtime.parallelism, job);
|
|
4631
|
+
} catch (err) {
|
|
4632
|
+
if (err instanceof WorkersUnavailable) return syncStrategy.mine(job);
|
|
4633
|
+
throw err;
|
|
4634
|
+
}
|
|
4566
4635
|
}
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4636
|
+
};
|
|
4637
|
+
}
|
|
4638
|
+
var workerThreadsStrategy = createWorkerThreadsStrategy();
|
|
4639
|
+
function mineInWorkers(Worker, parallelism, job) {
|
|
4640
|
+
const { factory, deployer, bytecodeHash, targetPrefix, maxAttempts, timeoutMs, onProgress, signal } = job;
|
|
4641
|
+
const workerCount = Math.max(1, Math.min(8, parallelism - 1));
|
|
4642
|
+
const perWorker = Math.ceil(maxAttempts / workerCount);
|
|
4643
|
+
const startedAt = Date.now();
|
|
4644
|
+
const workerSource = `
|
|
4645
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
4646
|
+
const { getCreate2Address, hexlify, isQuaiAddress, keccak256, randomBytes, solidityPacked } = require('quais');
|
|
4647
|
+
const { factory, deployer, bytecodeHash, targetPrefix, maxAttempts } = workerData;
|
|
4648
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
4649
|
+
const salt = hexlify(randomBytes(32));
|
|
4650
|
+
const fullSalt = keccak256(solidityPacked(['address', 'bytes32'], [deployer, salt]));
|
|
4651
|
+
const address = getCreate2Address(factory, fullSalt, bytecodeHash);
|
|
4652
|
+
if (address.toLowerCase().startsWith(targetPrefix) && isQuaiAddress(address)) {
|
|
4653
|
+
parentPort.postMessage({ type: 'result', salt, address, attempts: i + 1 });
|
|
4654
|
+
return;
|
|
4584
4655
|
}
|
|
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
|
-
)
|
|
4656
|
+
if ((i + 1) % ${PROGRESS_INTERVAL} === 0) parentPort.postMessage({ type: 'progress', attempts: i + 1 });
|
|
4657
|
+
}
|
|
4658
|
+
parentPort.postMessage({ type: 'exhausted', attempts: maxAttempts });
|
|
4659
|
+
`;
|
|
4660
|
+
return new Promise((resolve, reject) => {
|
|
4661
|
+
const workers = [];
|
|
4662
|
+
let settled = false;
|
|
4663
|
+
let exhausted = 0;
|
|
4664
|
+
let totalAttempts = 0;
|
|
4665
|
+
const progressByWorker = new Array(workerCount).fill(0);
|
|
4666
|
+
const cleanup = () => {
|
|
4667
|
+
clearTimeout(timer);
|
|
4668
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4669
|
+
for (const w of workers) void w.terminate();
|
|
4670
|
+
};
|
|
4671
|
+
const settle = (fn) => {
|
|
4672
|
+
if (settled) return;
|
|
4673
|
+
settled = true;
|
|
4674
|
+
cleanup();
|
|
4675
|
+
fn();
|
|
4676
|
+
};
|
|
4677
|
+
const timer = setTimeout(
|
|
4678
|
+
() => settle(
|
|
4679
|
+
() => reject(
|
|
4680
|
+
new SaltMiningError(
|
|
4681
|
+
`Salt mining timed out after ${timeoutMs} ms (~${totalAttempts} attempts across ${workerCount} workers).`,
|
|
4682
|
+
"Raise timeoutMs or maxAttempts."
|
|
4611
4683
|
)
|
|
4612
|
-
)
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4684
|
+
)
|
|
4685
|
+
),
|
|
4686
|
+
timeoutMs
|
|
4687
|
+
);
|
|
4688
|
+
const onAbort = () => settle(() => reject(new AbortError("Salt mining")));
|
|
4689
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4690
|
+
for (let i = 0; i < workerCount; i++) {
|
|
4691
|
+
let worker;
|
|
4692
|
+
try {
|
|
4693
|
+
worker = new Worker(workerSource, {
|
|
4619
4694
|
eval: true,
|
|
4620
4695
|
workerData: { factory, deployer, bytecodeHash, targetPrefix, maxAttempts: perWorker }
|
|
4621
4696
|
});
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4697
|
+
} catch (err) {
|
|
4698
|
+
settle(() => reject(new WorkersUnavailable(String(err))));
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
4701
|
+
workers.push(worker);
|
|
4702
|
+
worker.on("message", (msg) => {
|
|
4703
|
+
if (msg.type === "progress") {
|
|
4704
|
+
progressByWorker[i] = msg.attempts ?? 0;
|
|
4705
|
+
totalAttempts = progressByWorker.reduce((a, b) => a + b, 0);
|
|
4706
|
+
onProgress?.(totalAttempts);
|
|
4707
|
+
} else if (msg.type === "result") {
|
|
4708
|
+
settle(
|
|
4709
|
+
() => resolve({
|
|
4710
|
+
salt: msg.salt,
|
|
4711
|
+
predictedAddress: msg.address,
|
|
4712
|
+
attempts: totalAttempts + (msg.attempts ?? 0),
|
|
4713
|
+
durationMs: Date.now() - startedAt
|
|
4714
|
+
})
|
|
4715
|
+
);
|
|
4716
|
+
} else if (msg.type === "exhausted") {
|
|
4717
|
+
if (++exhausted === workerCount) {
|
|
4629
4718
|
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
|
-
)
|
|
4719
|
+
() => reject(
|
|
4720
|
+
new SaltMiningError(
|
|
4721
|
+
`No valid salt found for prefix ${targetPrefix} after ${maxAttempts} attempts.`,
|
|
4722
|
+
"Raise maxAttempts, or confirm the deployer address is on the shard you expect."
|
|
4645
4723
|
)
|
|
4646
|
-
)
|
|
4647
|
-
|
|
4724
|
+
)
|
|
4725
|
+
);
|
|
4648
4726
|
}
|
|
4649
|
-
}
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
});
|
|
4656
|
-
}
|
|
4657
|
-
};
|
|
4727
|
+
}
|
|
4728
|
+
});
|
|
4729
|
+
worker.on("error", (err) => settle(() => reject(new WorkersUnavailable(err.message))));
|
|
4730
|
+
}
|
|
4731
|
+
});
|
|
4732
|
+
}
|
|
4658
4733
|
function defaultStrategy() {
|
|
4659
4734
|
const hasNode = typeof globalThis.process?.versions?.node === "string";
|
|
4660
4735
|
return hasNode ? workerThreadsStrategy : syncStrategy;
|
|
@@ -4876,7 +4951,7 @@ function validateCreateParams(params) {
|
|
|
4876
4951
|
owners.forEach((owner, i) => {
|
|
4877
4952
|
assertQuaiAddress(owner, `owner[${i}]`);
|
|
4878
4953
|
const key = owner.toLowerCase();
|
|
4879
|
-
if (key ===
|
|
4954
|
+
if (key === ZERO_ADDRESS || key === SENTINEL_MODULES) {
|
|
4880
4955
|
throw new ValidationError(
|
|
4881
4956
|
`Owner ${owner} is reserved \u2014 the zero address and the module sentinel 0x\u202601 are rejected.`
|
|
4882
4957
|
);
|
|
@@ -4885,6 +4960,11 @@ function validateCreateParams(params) {
|
|
|
4885
4960
|
seen.add(key);
|
|
4886
4961
|
});
|
|
4887
4962
|
if (params.initialModules?.length) {
|
|
4963
|
+
if (params.initialModules.length > MAX_MODULES) {
|
|
4964
|
+
throw new ValidationError(
|
|
4965
|
+
`A vault may have at most ${MAX_MODULES} modules (got ${params.initialModules.length}).`
|
|
4966
|
+
);
|
|
4967
|
+
}
|
|
4888
4968
|
assertQuaiAddresses(params.initialModules, "initialModules");
|
|
4889
4969
|
}
|
|
4890
4970
|
if (params.initialDelegatecallTargets?.length) {
|
|
@@ -4922,7 +5002,8 @@ function extractCreatedVault(receipt, factoryAddress) {
|
|
|
4922
5002
|
}
|
|
4923
5003
|
|
|
4924
5004
|
// src/indexer/client.ts
|
|
4925
|
-
var
|
|
5005
|
+
var import_postgrest_js = require("@supabase/postgrest-js");
|
|
5006
|
+
var import_realtime_js = require("@supabase/realtime-js");
|
|
4926
5007
|
|
|
4927
5008
|
// src/indexer/schemas.ts
|
|
4928
5009
|
var schemas_exports = {};
|
|
@@ -5121,10 +5202,11 @@ function toNumber(value, fallback = 0) {
|
|
|
5121
5202
|
}
|
|
5122
5203
|
|
|
5123
5204
|
// src/indexer/client.ts
|
|
5124
|
-
var SDK_VERSION = "0.
|
|
5205
|
+
var SDK_VERSION = true ? "0.2.0" : "dev";
|
|
5125
5206
|
var IndexerClient = class {
|
|
5126
5207
|
config;
|
|
5127
5208
|
client;
|
|
5209
|
+
realtime = null;
|
|
5128
5210
|
healthCache = null;
|
|
5129
5211
|
inflightHealth = null;
|
|
5130
5212
|
/** Health results are reused for this long to keep read paths cheap. */
|
|
@@ -5132,19 +5214,46 @@ var IndexerClient = class {
|
|
|
5132
5214
|
constructor(config, options = {}) {
|
|
5133
5215
|
this.config = config;
|
|
5134
5216
|
this.healthCacheMs = options.healthCacheMs ?? 5e3;
|
|
5135
|
-
this.client =
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5217
|
+
this.client = new import_postgrest_js.PostgrestClient(`${baseUrl(config.url)}/rest/v1`, {
|
|
5218
|
+
headers: {
|
|
5219
|
+
apikey: config.anonKey,
|
|
5220
|
+
Authorization: `Bearer ${config.anonKey}`,
|
|
5221
|
+
"x-client-info": `quaivault-sdk/${SDK_VERSION}`
|
|
5222
|
+
},
|
|
5223
|
+
schema: config.schema
|
|
5139
5224
|
});
|
|
5140
5225
|
}
|
|
5141
|
-
/**
|
|
5142
|
-
get
|
|
5226
|
+
/** The PostgREST client, for queries the SDK does not wrap. */
|
|
5227
|
+
get rest() {
|
|
5143
5228
|
return this.client;
|
|
5144
5229
|
}
|
|
5145
5230
|
from(table) {
|
|
5146
5231
|
return this.client.from(table);
|
|
5147
5232
|
}
|
|
5233
|
+
/**
|
|
5234
|
+
* Realtime client, constructed on first use.
|
|
5235
|
+
*
|
|
5236
|
+
* Lazy because it opens a WebSocket and starts a heartbeat, and the large majority
|
|
5237
|
+
* of SDK usage never calls `watch()`. Nothing here should cost a socket unless
|
|
5238
|
+
* somebody asked to follow a vault.
|
|
5239
|
+
*/
|
|
5240
|
+
realtimeClient() {
|
|
5241
|
+
if (!this.realtime) {
|
|
5242
|
+
this.realtime = new import_realtime_js.RealtimeClient(
|
|
5243
|
+
`${baseUrl(this.config.url).replace(/^http/, "ws")}/realtime/v1`,
|
|
5244
|
+
{ params: { apikey: this.config.anonKey } }
|
|
5245
|
+
);
|
|
5246
|
+
}
|
|
5247
|
+
return this.realtime;
|
|
5248
|
+
}
|
|
5249
|
+
/** Open a Realtime channel. See `watchVault`, which is what callers should use. */
|
|
5250
|
+
channel(name) {
|
|
5251
|
+
return this.realtimeClient().channel(name);
|
|
5252
|
+
}
|
|
5253
|
+
/** Close a channel opened by {@link channel}. */
|
|
5254
|
+
async removeChannel(channel) {
|
|
5255
|
+
await this.realtimeClient().removeChannel(channel);
|
|
5256
|
+
}
|
|
5148
5257
|
/**
|
|
5149
5258
|
* Run a query, parse each row, and surface failures as `IndexerQueryError`.
|
|
5150
5259
|
*
|
|
@@ -5162,6 +5271,36 @@ var IndexerClient = class {
|
|
|
5162
5271
|
}
|
|
5163
5272
|
return (data ?? []).map((row) => schema.parse(row));
|
|
5164
5273
|
}
|
|
5274
|
+
/**
|
|
5275
|
+
* Paged select: an exact `hasMore`, an approximate `total`.
|
|
5276
|
+
*
|
|
5277
|
+
* Both halves of that are deliberate. An `exact` count makes Postgres scan every
|
|
5278
|
+
* matching row on every page request, which is invisible on a young vault and
|
|
5279
|
+
* quadratic on a busy one — so the count is `estimated`, cheap and good enough for
|
|
5280
|
+
* "roughly how much history is there".
|
|
5281
|
+
*
|
|
5282
|
+
* But `hasMore` derived from an estimate would be a lie a caller can act on: a
|
|
5283
|
+
* paging loop that stops early drops real rows. So callers request one row beyond
|
|
5284
|
+
* the page and this trims it, which answers "is there another page" exactly, for
|
|
5285
|
+
* the cost of a single extra row.
|
|
5286
|
+
*/
|
|
5287
|
+
async selectPage(table, schema, limit, build) {
|
|
5288
|
+
const { data, error, count } = await build(this.client.from(table));
|
|
5289
|
+
if (error) {
|
|
5290
|
+
throw new IndexerQueryError(`Indexer query on "${table}" failed: ${error.message}`, error);
|
|
5291
|
+
}
|
|
5292
|
+
const rows = data ?? [];
|
|
5293
|
+
const hasMore = rows.length > limit;
|
|
5294
|
+
const parsed = (hasMore ? rows.slice(0, limit) : rows).map(
|
|
5295
|
+
(row) => schema.parse(row)
|
|
5296
|
+
);
|
|
5297
|
+
return {
|
|
5298
|
+
data: parsed,
|
|
5299
|
+
// Never report a total below what was just handed out, however the estimate lands.
|
|
5300
|
+
total: Math.max(count ?? 0, parsed.length),
|
|
5301
|
+
hasMore
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5165
5304
|
/** Single-row variant. Returns null for PostgREST's "no rows" code. */
|
|
5166
5305
|
async selectOne(table, schema, build) {
|
|
5167
5306
|
const { data, error } = await build(this.client.from(table));
|
|
@@ -5216,7 +5355,7 @@ var IndexerClient = class {
|
|
|
5216
5355
|
const pollIntervalMs = options.pollIntervalMs ?? 1500;
|
|
5217
5356
|
const deadline = Date.now() + timeoutMs;
|
|
5218
5357
|
for (; ; ) {
|
|
5219
|
-
if (options.signal?.aborted) throw new
|
|
5358
|
+
if (options.signal?.aborted) throw new AbortError("Waiting for the indexer");
|
|
5220
5359
|
const state = await this.state();
|
|
5221
5360
|
const head = state?.lastIndexedBlock ?? 0;
|
|
5222
5361
|
if (head >= blockNumber) return { reached: true, lastIndexedBlock: head };
|
|
@@ -5228,12 +5367,12 @@ var IndexerClient = class {
|
|
|
5228
5367
|
}
|
|
5229
5368
|
async probeHealth() {
|
|
5230
5369
|
if (this.config.healthUrl) {
|
|
5370
|
+
const controller = new AbortController();
|
|
5371
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
5231
5372
|
try {
|
|
5232
|
-
const controller = new AbortController();
|
|
5233
|
-
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
5234
5373
|
const res = await fetch(new URL("/health", this.config.healthUrl), {
|
|
5235
5374
|
signal: controller.signal
|
|
5236
|
-
})
|
|
5375
|
+
});
|
|
5237
5376
|
const body = await res.json();
|
|
5238
5377
|
const d = body.details ?? {};
|
|
5239
5378
|
return {
|
|
@@ -5245,6 +5384,8 @@ var IndexerClient = class {
|
|
|
5245
5384
|
...res.ok ? {} : { error: `health endpoint returned ${res.status}` }
|
|
5246
5385
|
};
|
|
5247
5386
|
} catch {
|
|
5387
|
+
} finally {
|
|
5388
|
+
clearTimeout(timer);
|
|
5248
5389
|
}
|
|
5249
5390
|
}
|
|
5250
5391
|
try {
|
|
@@ -5267,10 +5408,14 @@ var IndexerClient = class {
|
|
|
5267
5408
|
}
|
|
5268
5409
|
}
|
|
5269
5410
|
};
|
|
5411
|
+
function baseUrl(url) {
|
|
5412
|
+
return url.replace(/\/+$/, "");
|
|
5413
|
+
}
|
|
5270
5414
|
|
|
5271
5415
|
// src/indexer/queries.ts
|
|
5272
5416
|
var DEFAULT_LIMIT = 50;
|
|
5273
5417
|
var MAX_LIMIT = 200;
|
|
5418
|
+
var CONFIRMATION_CHUNK = 40;
|
|
5274
5419
|
function lower(address2) {
|
|
5275
5420
|
return address2.toLowerCase();
|
|
5276
5421
|
}
|
|
@@ -5329,14 +5474,40 @@ var IndexerQueries = class {
|
|
|
5329
5474
|
(q) => q.select("*").eq("wallet_address", lower(vault2)).eq("tx_hash", txHash.toLowerCase()).single()
|
|
5330
5475
|
);
|
|
5331
5476
|
}
|
|
5477
|
+
/**
|
|
5478
|
+
* Several transactions by hash, chunked for the same reasons as
|
|
5479
|
+
* {@link activeConfirmationsBatch} — hashes are long, and they go in the query string.
|
|
5480
|
+
*
|
|
5481
|
+
* Rows come back in no particular order and hashes with no row are simply absent;
|
|
5482
|
+
* the caller matches them up.
|
|
5483
|
+
*/
|
|
5484
|
+
async transactionsByHash(vault2, txHashes) {
|
|
5485
|
+
if (txHashes.length === 0) return [];
|
|
5486
|
+
const hashes = txHashes.map((h) => h.toLowerCase());
|
|
5487
|
+
const chunks = [];
|
|
5488
|
+
for (let i = 0; i < hashes.length; i += CONFIRMATION_CHUNK) {
|
|
5489
|
+
chunks.push(hashes.slice(i, i + CONFIRMATION_CHUNK));
|
|
5490
|
+
}
|
|
5491
|
+
const pages = await Promise.all(
|
|
5492
|
+
chunks.map(
|
|
5493
|
+
(chunk) => this.client.select(
|
|
5494
|
+
"transactions",
|
|
5495
|
+
TransactionSchema,
|
|
5496
|
+
(q) => q.select("*").eq("wallet_address", lower(vault2)).in("tx_hash", chunk)
|
|
5497
|
+
)
|
|
5498
|
+
)
|
|
5499
|
+
);
|
|
5500
|
+
return pages.flat();
|
|
5501
|
+
}
|
|
5332
5502
|
async transactionHistory(vault2, options = {}) {
|
|
5333
5503
|
const { limit, offset } = bounds(options);
|
|
5334
5504
|
const statuses = options.status ?? ["executed", "cancelled", "expired", "failed"];
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5505
|
+
return this.client.selectPage(
|
|
5506
|
+
"transactions",
|
|
5507
|
+
TransactionSchema,
|
|
5508
|
+
limit,
|
|
5509
|
+
(q) => q.select("*", { count: "estimated" }).eq("wallet_address", lower(vault2)).in("status", statuses).order("submitted_at_block", { ascending: false }).range(offset, offset + limit)
|
|
5510
|
+
);
|
|
5340
5511
|
}
|
|
5341
5512
|
/** All confirmations for one transaction, including revoked ones. */
|
|
5342
5513
|
async confirmations(vault2, txHash) {
|
|
@@ -5347,26 +5518,41 @@ var IndexerQueries = class {
|
|
|
5347
5518
|
);
|
|
5348
5519
|
}
|
|
5349
5520
|
/**
|
|
5350
|
-
* Active confirmations for many transactions in
|
|
5521
|
+
* Active confirmations for many transactions, in as few round trips as is safe.
|
|
5351
5522
|
*
|
|
5352
5523
|
* "Active" here means not revoked. It does NOT mean the confirming address is
|
|
5353
5524
|
* still an owner — the indexer does not deactivate confirmations when an owner is
|
|
5354
5525
|
* removed, while the contract invalidates them via approval epochs. Callers must
|
|
5355
5526
|
* intersect with the active owner set; `Vault` does this for you.
|
|
5527
|
+
*
|
|
5528
|
+
* Chunked rather than issued as one `in(...)`, because a single filter fails two
|
|
5529
|
+
* ways at scale. PostgREST reads filters from the query string, and each 32-byte
|
|
5530
|
+
* hash costs ~70 characters there — a full {@link MAX_LIMIT} page of hashes builds a
|
|
5531
|
+
* ~14 KB request line, past the 8 KB cap most reverse proxies apply by default. The
|
|
5532
|
+
* response is bounded too: {@link CONFIRMATION_CHUNK} transactions can carry at most
|
|
5533
|
+
* `chunk × MAX_OWNERS` rows, and the chunk is sized to keep that under the
|
|
5534
|
+
* `max-rows` ceiling PostgREST deployments commonly set, so a busy vault cannot be
|
|
5535
|
+
* silently short-served.
|
|
5356
5536
|
*/
|
|
5357
5537
|
async activeConfirmationsBatch(vault2, txHashes) {
|
|
5358
5538
|
const result = /* @__PURE__ */ new Map();
|
|
5359
5539
|
for (const hash of txHashes) result.set(hash.toLowerCase(), []);
|
|
5360
5540
|
if (txHashes.length === 0) return result;
|
|
5361
|
-
const
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5541
|
+
const hashes = txHashes.map((h) => h.toLowerCase());
|
|
5542
|
+
const chunks = [];
|
|
5543
|
+
for (let i = 0; i < hashes.length; i += CONFIRMATION_CHUNK) {
|
|
5544
|
+
chunks.push(hashes.slice(i, i + CONFIRMATION_CHUNK));
|
|
5545
|
+
}
|
|
5546
|
+
const pages = await Promise.all(
|
|
5547
|
+
chunks.map(
|
|
5548
|
+
(chunk) => this.client.select(
|
|
5549
|
+
"confirmations",
|
|
5550
|
+
ConfirmationSchema,
|
|
5551
|
+
(q) => q.select("*").eq("wallet_address", lower(vault2)).in("tx_hash", chunk).eq("is_active", true)
|
|
5552
|
+
)
|
|
5553
|
+
)
|
|
5368
5554
|
);
|
|
5369
|
-
for (const row of
|
|
5555
|
+
for (const row of pages.flat()) {
|
|
5370
5556
|
const key = row.tx_hash.toLowerCase();
|
|
5371
5557
|
const list = result.get(key);
|
|
5372
5558
|
if (list) list.push(row);
|
|
@@ -5394,19 +5580,52 @@ var IndexerQueries = class {
|
|
|
5394
5580
|
// ---- value movement ------------------------------------------------------
|
|
5395
5581
|
async deposits(vault2, options = {}) {
|
|
5396
5582
|
const { limit, offset } = bounds(options);
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5583
|
+
return this.client.selectPage(
|
|
5584
|
+
"deposits",
|
|
5585
|
+
DepositSchema,
|
|
5586
|
+
limit,
|
|
5587
|
+
(q) => q.select("*", { count: "estimated" }).eq("wallet_address", lower(vault2)).order("deposited_at_block", { ascending: false }).range(offset, offset + limit)
|
|
5588
|
+
);
|
|
5402
5589
|
}
|
|
5403
5590
|
async tokenTransfers(vault2, options = {}) {
|
|
5404
5591
|
const { limit, offset } = bounds(options);
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5592
|
+
return this.client.selectPage(
|
|
5593
|
+
"token_transfers",
|
|
5594
|
+
TokenTransferSchema,
|
|
5595
|
+
limit,
|
|
5596
|
+
(q) => q.select("*", { count: "estimated" }).eq("wallet_address", lower(vault2)).order("block_number", { ascending: false }).range(offset, offset + limit)
|
|
5597
|
+
);
|
|
5598
|
+
}
|
|
5599
|
+
/**
|
|
5600
|
+
* Scan up to `budget` of the most recent transfer rows, paging past {@link MAX_LIMIT}.
|
|
5601
|
+
*
|
|
5602
|
+
* `tokenTransfers` clamps a single request, so a caller asking for more than the cap
|
|
5603
|
+
* silently received a short page — and then computed "did I see everything?" from
|
|
5604
|
+
* that short page's `hasMore`, which reported truncation the caller had not actually
|
|
5605
|
+
* hit. Paging here keeps the caller's budget meaningful and makes the returned
|
|
5606
|
+
* `hasMore` mean what it says: rows exist beyond the budget that was scanned.
|
|
5607
|
+
*
|
|
5608
|
+
* Pages by offset over a descending order, so a transfer landing mid-scan can shift
|
|
5609
|
+
* rows by one. That is acceptable for token *discovery* — a duplicate collapses into
|
|
5610
|
+
* the same map key and a missed row costs at most one candidate that the next call
|
|
5611
|
+
* picks up. Do not reuse this for anything that must see each row exactly once.
|
|
5612
|
+
*/
|
|
5613
|
+
async tokenTransferScan(vault2, budget) {
|
|
5614
|
+
const target = Math.max(1, Math.floor(budget));
|
|
5615
|
+
const data = [];
|
|
5616
|
+
let total = 0;
|
|
5617
|
+
let hasMore = false;
|
|
5618
|
+
while (data.length < target) {
|
|
5619
|
+
const page = await this.tokenTransfers(vault2, {
|
|
5620
|
+
limit: Math.min(MAX_LIMIT, target - data.length),
|
|
5621
|
+
offset: data.length
|
|
5622
|
+
});
|
|
5623
|
+
data.push(...page.data);
|
|
5624
|
+
total = page.total;
|
|
5625
|
+
hasMore = page.hasMore;
|
|
5626
|
+
if (page.data.length === 0 || !page.hasMore) break;
|
|
5627
|
+
}
|
|
5628
|
+
return { data, total, hasMore };
|
|
5410
5629
|
}
|
|
5411
5630
|
async tokens(addresses) {
|
|
5412
5631
|
if (addresses.length === 0) return [];
|
|
@@ -5629,8 +5848,6 @@ function deriveRecoveryStatus(state, at = nowSeconds()) {
|
|
|
5629
5848
|
}
|
|
5630
5849
|
|
|
5631
5850
|
// src/recovery.ts
|
|
5632
|
-
var SENTINEL = "0x0000000000000000000000000000000000000001";
|
|
5633
|
-
var ZERO = "0x0000000000000000000000000000000000000000";
|
|
5634
5851
|
var RecoveryModule = class {
|
|
5635
5852
|
constructor(ctx) {
|
|
5636
5853
|
this.ctx = ctx;
|
|
@@ -5652,14 +5869,23 @@ var RecoveryModule = class {
|
|
|
5652
5869
|
get vault() {
|
|
5653
5870
|
return this.ctx.vaultAddress;
|
|
5654
5871
|
}
|
|
5872
|
+
/**
|
|
5873
|
+
* The vault this module acts on, through the same retrying facade every other vault
|
|
5874
|
+
* read in the SDK uses. Recovery reaches into the vault for three checks — is the
|
|
5875
|
+
* module enabled, is the caller an owner, who are the current owners — and each of
|
|
5876
|
+
* them gates a write, so none of them should be the one read that silently skips
|
|
5877
|
+
* transient-failure handling.
|
|
5878
|
+
*/
|
|
5879
|
+
vaultContract() {
|
|
5880
|
+
return new VaultContract(this.ctx.connection.vault(this.vault), this.ctx.connection.retry);
|
|
5881
|
+
}
|
|
5655
5882
|
// =========================================================================
|
|
5656
5883
|
// Reads
|
|
5657
5884
|
// =========================================================================
|
|
5658
5885
|
/** Whether the module is currently enabled on the vault. */
|
|
5659
5886
|
async isEnabled() {
|
|
5660
5887
|
if (!this.ctx.moduleAddress) return false;
|
|
5661
|
-
|
|
5662
|
-
return vault2.getFunction("isModuleEnabled(address)")(this.address);
|
|
5888
|
+
return this.vaultContract().isModuleEnabled(this.address);
|
|
5663
5889
|
}
|
|
5664
5890
|
async config() {
|
|
5665
5891
|
const raw = await this.contract().getRecoveryConfig(this.vault);
|
|
@@ -5717,7 +5943,7 @@ var RecoveryModule = class {
|
|
|
5717
5943
|
*/
|
|
5718
5944
|
async history(options = {}) {
|
|
5719
5945
|
const queries = this.ctx.queries;
|
|
5720
|
-
if (!queries)
|
|
5946
|
+
if (!queries) throw new NoIndexerError("Listing recovery history");
|
|
5721
5947
|
const rows = await queries.recoveryHistory(this.vault, options);
|
|
5722
5948
|
return rows.map((row) => ({
|
|
5723
5949
|
hash: row.recovery_hash,
|
|
@@ -5853,14 +6079,13 @@ var RecoveryModule = class {
|
|
|
5853
6079
|
{ retryableAt: request.executionTime }
|
|
5854
6080
|
);
|
|
5855
6081
|
}
|
|
5856
|
-
const
|
|
5857
|
-
const currentOwners = await vault2.getFunction("getOwners()")();
|
|
6082
|
+
const currentOwners = await this.vaultContract().getOwners();
|
|
5858
6083
|
const current = new Set(Array.from(currentOwners).map((o) => String(o).toLowerCase()));
|
|
5859
6084
|
const overlap = request.newOwners.filter((o) => current.has(o.toLowerCase())).length;
|
|
5860
6085
|
const peak = current.size + request.newOwners.length - overlap;
|
|
5861
|
-
if (peak >
|
|
6086
|
+
if (peak > MAX_OWNERS) {
|
|
5862
6087
|
throw new PreconditionError(
|
|
5863
|
-
`Executing this recovery would transiently hold ${peak} owners, above the
|
|
6088
|
+
`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
6089
|
{
|
|
5865
6090
|
remediation: "Run a recovery that retains at least one existing owner, then a second recovery to replace it."
|
|
5866
6091
|
}
|
|
@@ -5875,8 +6100,7 @@ var RecoveryModule = class {
|
|
|
5875
6100
|
async cancel(recoveryHash) {
|
|
5876
6101
|
const hash = normalizeTxHash(recoveryHash, "recovery hash");
|
|
5877
6102
|
const caller = await this.ctx.connection.address();
|
|
5878
|
-
const
|
|
5879
|
-
const isOwner = await vault2.getFunction("isOwner(address)")(caller);
|
|
6103
|
+
const isOwner = await this.vaultContract().isOwner(caller);
|
|
5880
6104
|
if (!isOwner) {
|
|
5881
6105
|
throw new PreconditionError(
|
|
5882
6106
|
`Only a current owner of the vault can cancel a recovery. ${caller} is not one.`
|
|
@@ -5907,7 +6131,7 @@ var RecoveryModule = class {
|
|
|
5907
6131
|
this.isGuardian(address2),
|
|
5908
6132
|
this.contract().recoveryApprovals(this.vault, hash, address2),
|
|
5909
6133
|
this.isEnabled(),
|
|
5910
|
-
this.
|
|
6134
|
+
this.vaultContract().isOwner(address2)
|
|
5911
6135
|
]);
|
|
5912
6136
|
const now = nowSeconds();
|
|
5913
6137
|
const terminal = request.status === "executed" || request.status === "cancelled";
|
|
@@ -6032,7 +6256,7 @@ var RecoveryModule = class {
|
|
|
6032
6256
|
const seen = /* @__PURE__ */ new Set();
|
|
6033
6257
|
for (const owner of newOwners) {
|
|
6034
6258
|
const key = owner.toLowerCase();
|
|
6035
|
-
if (key ===
|
|
6259
|
+
if (key === ZERO_ADDRESS || key === SENTINEL_MODULES) {
|
|
6036
6260
|
throw new ValidationError(
|
|
6037
6261
|
`New owner ${owner} is reserved \u2014 the zero address and the module sentinel 0x\u202601 are rejected.`
|
|
6038
6262
|
);
|
|
@@ -6114,15 +6338,60 @@ function terminalReason(request) {
|
|
|
6114
6338
|
|
|
6115
6339
|
// src/balances.ts
|
|
6116
6340
|
var import_quais10 = require("quais");
|
|
6341
|
+
|
|
6342
|
+
// src/chain/token-contract.ts
|
|
6343
|
+
var TOKEN_RETRY = { maxAttempts: 2, baseDelayMs: 150 };
|
|
6344
|
+
var TokenContract = class {
|
|
6345
|
+
constructor(contract, retry = {}) {
|
|
6346
|
+
this.contract = contract;
|
|
6347
|
+
this.retry = { ...TOKEN_RETRY, ...retry };
|
|
6348
|
+
}
|
|
6349
|
+
contract;
|
|
6350
|
+
retry;
|
|
6351
|
+
read(signature, ...args) {
|
|
6352
|
+
return withRetry(() => this.contract.getFunction(signature)(...args), this.retry);
|
|
6353
|
+
}
|
|
6354
|
+
/** ERC20 units held, or — for ERC721 — the number of tokens held. */
|
|
6355
|
+
balanceOf(owner) {
|
|
6356
|
+
return this.read("balanceOf(address)", owner);
|
|
6357
|
+
}
|
|
6358
|
+
/** ERC721 only. Reverts for a burned or never-minted id. */
|
|
6359
|
+
ownerOf(tokenId) {
|
|
6360
|
+
return this.read("ownerOf(uint256)", tokenId);
|
|
6361
|
+
}
|
|
6362
|
+
/** ERC1155 only. One call for many ids, so it stays a single round trip. */
|
|
6363
|
+
balanceOfBatch(owners, ids) {
|
|
6364
|
+
return this.read("balanceOfBatch(address[],uint256[])", owners, ids);
|
|
6365
|
+
}
|
|
6366
|
+
};
|
|
6367
|
+
|
|
6368
|
+
// src/pool.ts
|
|
6369
|
+
async function mapPooled(items, limit, fn) {
|
|
6370
|
+
const results = new Array(items.length);
|
|
6371
|
+
let next = 0;
|
|
6372
|
+
const worker = async () => {
|
|
6373
|
+
for (; ; ) {
|
|
6374
|
+
const i = next++;
|
|
6375
|
+
if (i >= items.length) return;
|
|
6376
|
+
results[i] = await fn(items[i], i);
|
|
6377
|
+
}
|
|
6378
|
+
};
|
|
6379
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
6380
|
+
return results;
|
|
6381
|
+
}
|
|
6382
|
+
var DEFAULT_CONCURRENCY = 8;
|
|
6383
|
+
|
|
6384
|
+
// src/balances.ts
|
|
6117
6385
|
async function loadBalances(connection, queries, vault2, options = {}) {
|
|
6118
6386
|
const verify = options.verify !== false;
|
|
6119
6387
|
const maxTokens = options.maxTokens ?? 50;
|
|
6120
6388
|
const transferScanLimit = options.transferScanLimit ?? 500;
|
|
6121
6389
|
const maxTokenIdChecks = options.maxTokenIdChecks ?? 100;
|
|
6390
|
+
const concurrency = Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY);
|
|
6122
6391
|
const address2 = (0, import_quais10.getAddress)(vault2);
|
|
6123
6392
|
const native = BigInt(await connection.provider.getBalance(address2));
|
|
6124
6393
|
if (!queries) return { native, tokens: [] };
|
|
6125
|
-
const transfers = await queries.
|
|
6394
|
+
const transfers = await queries.tokenTransferScan(address2, transferScanLimit);
|
|
6126
6395
|
const seen = /* @__PURE__ */ new Map();
|
|
6127
6396
|
for (const row of transfers.data) {
|
|
6128
6397
|
const key = row.token_address.toLowerCase();
|
|
@@ -6141,17 +6410,18 @@ async function loadBalances(connection, queries, vault2, options = {}) {
|
|
|
6141
6410
|
}
|
|
6142
6411
|
const metadata = await queries.tokens(candidates);
|
|
6143
6412
|
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
|
-
|
|
6413
|
+
const balances = await mapPooled(
|
|
6414
|
+
candidates,
|
|
6415
|
+
concurrency,
|
|
6416
|
+
(token2) => buildBalance(
|
|
6417
|
+
connection,
|
|
6418
|
+
address2,
|
|
6419
|
+
token2,
|
|
6420
|
+
byAddress.get(token2),
|
|
6421
|
+
seen.get(token2) ?? [],
|
|
6422
|
+
verify,
|
|
6423
|
+
maxTokenIdChecks,
|
|
6424
|
+
concurrency
|
|
6155
6425
|
)
|
|
6156
6426
|
);
|
|
6157
6427
|
return {
|
|
@@ -6161,30 +6431,35 @@ async function loadBalances(connection, queries, vault2, options = {}) {
|
|
|
6161
6431
|
...Object.keys(truncated).length ? { truncated } : {}
|
|
6162
6432
|
};
|
|
6163
6433
|
}
|
|
6164
|
-
async function buildBalance(connection, vault2, token2, meta, transfers, verify, maxTokenIdChecks) {
|
|
6434
|
+
async function buildBalance(connection, vault2, token2, meta, transfers, verify, maxTokenIdChecks, concurrency) {
|
|
6165
6435
|
const standard = meta?.standard ?? inferStandard(transfers);
|
|
6166
6436
|
const replayed = replayBalance(transfers, standard);
|
|
6167
6437
|
const base = {
|
|
6168
6438
|
token: (0, import_quais10.getAddress)(token2),
|
|
6169
6439
|
standard,
|
|
6170
|
-
|
|
6171
|
-
|
|
6440
|
+
// Deployer-controlled strings headed for someone's terminal. See `src/text.ts`.
|
|
6441
|
+
symbol: sanitizeText(meta?.symbol, 32) || "???",
|
|
6442
|
+
name: sanitizeText(meta?.name, 64) || "Unknown token",
|
|
6172
6443
|
decimals: meta?.decimals ?? (standard === "ERC20" ? 18 : 0),
|
|
6173
6444
|
balance: replayed.balance,
|
|
6174
6445
|
...replayed.tokenIds.length > 0 ? { tokenIds: replayed.tokenIds } : {},
|
|
6175
6446
|
verified: false
|
|
6176
6447
|
};
|
|
6177
6448
|
if (!verify) return base;
|
|
6449
|
+
const contract = new TokenContract(connection.token(token2, standard), connection.retry);
|
|
6178
6450
|
try {
|
|
6179
6451
|
if (standard === "ERC20") {
|
|
6180
|
-
|
|
6181
|
-
const balance = await contract2.getFunction("balanceOf(address)")(vault2);
|
|
6182
|
-
return { ...base, balance: BigInt(balance), verified: true };
|
|
6452
|
+
return { ...base, balance: BigInt(await contract.balanceOf(vault2)), verified: true };
|
|
6183
6453
|
}
|
|
6184
6454
|
if (standard === "ERC721") {
|
|
6185
|
-
const
|
|
6186
|
-
const
|
|
6187
|
-
|
|
6455
|
+
const count = await contract.balanceOf(vault2);
|
|
6456
|
+
const owned = await filterOwned(
|
|
6457
|
+
contract,
|
|
6458
|
+
vault2,
|
|
6459
|
+
replayed.tokenIds,
|
|
6460
|
+
maxTokenIdChecks,
|
|
6461
|
+
concurrency
|
|
6462
|
+
);
|
|
6188
6463
|
return {
|
|
6189
6464
|
...base,
|
|
6190
6465
|
balance: BigInt(count),
|
|
@@ -6193,10 +6468,9 @@ async function buildBalance(connection, vault2, token2, meta, transfers, verify,
|
|
|
6193
6468
|
verified: true
|
|
6194
6469
|
};
|
|
6195
6470
|
}
|
|
6196
|
-
const contract = new import_quais10.Contract(token2, Erc1155Abi, connection.provider);
|
|
6197
6471
|
const ids = replayed.tokenIds;
|
|
6198
6472
|
if (ids.length === 0) return { ...base, verified: true };
|
|
6199
|
-
const amounts = await contract.
|
|
6473
|
+
const amounts = await contract.balanceOfBatch(
|
|
6200
6474
|
ids.map(() => vault2),
|
|
6201
6475
|
ids
|
|
6202
6476
|
);
|
|
@@ -6241,18 +6515,16 @@ function replayBalance(transfers, standard) {
|
|
|
6241
6515
|
}
|
|
6242
6516
|
return { balance, tokenIds };
|
|
6243
6517
|
}
|
|
6244
|
-
async function filterOwned(contract, vault2, ids, limit) {
|
|
6518
|
+
async function filterOwned(contract, vault2, ids, limit, concurrency) {
|
|
6245
6519
|
if (ids.length === 0) return [];
|
|
6246
|
-
const results = await
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
})
|
|
6255
|
-
);
|
|
6520
|
+
const results = await mapPooled(ids.slice(0, limit), concurrency, async (id) => {
|
|
6521
|
+
try {
|
|
6522
|
+
const owner = await contract.ownerOf(id);
|
|
6523
|
+
return owner.toLowerCase() === vault2.toLowerCase() ? id : null;
|
|
6524
|
+
} catch {
|
|
6525
|
+
return null;
|
|
6526
|
+
}
|
|
6527
|
+
});
|
|
6256
6528
|
return results.filter((id) => id !== null);
|
|
6257
6529
|
}
|
|
6258
6530
|
function inferStandard(transfers) {
|
|
@@ -6288,7 +6560,7 @@ function watchVault(client, vault2, handler, options = {}) {
|
|
|
6288
6560
|
const topics = options.topics ?? DEFAULT_TOPICS;
|
|
6289
6561
|
const address2 = vault2.toLowerCase();
|
|
6290
6562
|
const schema = client.config.schema;
|
|
6291
|
-
const channel = client.
|
|
6563
|
+
const channel = client.channel(`quaivault:${schema}:${address2}`);
|
|
6292
6564
|
for (const topic of topics) {
|
|
6293
6565
|
const table = TABLE_FOR[topic];
|
|
6294
6566
|
channel.on(
|
|
@@ -6318,7 +6590,7 @@ function watchVault(client, vault2, handler, options = {}) {
|
|
|
6318
6590
|
return {
|
|
6319
6591
|
topics,
|
|
6320
6592
|
async unsubscribe() {
|
|
6321
|
-
await client.
|
|
6593
|
+
await client.removeChannel(channel);
|
|
6322
6594
|
}
|
|
6323
6595
|
};
|
|
6324
6596
|
}
|
|
@@ -6798,7 +7070,7 @@ function extractProposedTxHash(receipt, vault2) {
|
|
|
6798
7070
|
}
|
|
6799
7071
|
|
|
6800
7072
|
// src/vault.ts
|
|
6801
|
-
var Vault = class {
|
|
7073
|
+
var Vault = class _Vault {
|
|
6802
7074
|
address;
|
|
6803
7075
|
/** Guardian-based recovery for this vault. */
|
|
6804
7076
|
recovery;
|
|
@@ -6841,6 +7113,21 @@ var Vault = class {
|
|
|
6841
7113
|
if (!this.ctx.queries) throw new NoIndexerError(operation);
|
|
6842
7114
|
return this.ctx.queries;
|
|
6843
7115
|
}
|
|
7116
|
+
/**
|
|
7117
|
+
* The indexer's head, for stamping onto records read from it.
|
|
7118
|
+
*
|
|
7119
|
+
* Reads the cached health result rather than querying `indexer_state` directly.
|
|
7120
|
+
* `useIndexer()` has just called `health()` on this same code path and the result is
|
|
7121
|
+
* cached, so this is free — whereas a `state()` call is a second round trip per
|
|
7122
|
+
* hydration, paid on every indexed read purely to fill one advisory field.
|
|
7123
|
+
*
|
|
7124
|
+
* Undefined when the indexer is not answering: a head of 0 would read as "indexed at
|
|
7125
|
+
* genesis" rather than "unknown".
|
|
7126
|
+
*/
|
|
7127
|
+
async indexerHead() {
|
|
7128
|
+
const health = await this.ctx.indexer?.health();
|
|
7129
|
+
return health?.available ? health.lastIndexedBlock : void 0;
|
|
7130
|
+
}
|
|
6844
7131
|
contract(write = false) {
|
|
6845
7132
|
return new VaultContract(this.ctx.connection.vault(this.address, write), this.ctx.connection.retry);
|
|
6846
7133
|
}
|
|
@@ -6868,8 +7155,48 @@ var Vault = class {
|
|
|
6868
7155
|
balance: BigInt(balance)
|
|
6869
7156
|
};
|
|
6870
7157
|
}
|
|
6871
|
-
/**
|
|
7158
|
+
/**
|
|
7159
|
+
* Capture owners and threshold once, for reuse across a burst of reads.
|
|
7160
|
+
*
|
|
7161
|
+
* Pair with {@link pinned}:
|
|
7162
|
+
*
|
|
7163
|
+
* ```ts
|
|
7164
|
+
* const view = await vault.view();
|
|
7165
|
+
* const pinned = vault.pinned(view);
|
|
7166
|
+
* // every read below shares one owner/threshold read
|
|
7167
|
+
* const txs = await pinned.transactions(hashes);
|
|
7168
|
+
* ```
|
|
7169
|
+
*/
|
|
7170
|
+
async view() {
|
|
7171
|
+
const fromIndexer = await this.useIndexer();
|
|
7172
|
+
const [owners, threshold, indexedAtBlock] = await Promise.all([
|
|
7173
|
+
this.owners(),
|
|
7174
|
+
this.threshold(),
|
|
7175
|
+
this.indexerHead()
|
|
7176
|
+
]);
|
|
7177
|
+
return {
|
|
7178
|
+
owners,
|
|
7179
|
+
threshold,
|
|
7180
|
+
...indexedAtBlock !== void 0 ? { indexedAtBlock } : {},
|
|
7181
|
+
capturedAt: nowSeconds(),
|
|
7182
|
+
source: fromIndexer ? "indexer" : "chain"
|
|
7183
|
+
};
|
|
7184
|
+
}
|
|
7185
|
+
/**
|
|
7186
|
+
* A handle that answers {@link owners} and {@link threshold} from `view` instead of
|
|
7187
|
+
* re-reading them.
|
|
7188
|
+
*
|
|
7189
|
+
* Reads only. Write preconditions go through `chainOwners` / `chainThreshold`, which
|
|
7190
|
+
* bypass the snapshot by construction — pinning a view can never cause this SDK to
|
|
7191
|
+
* sign against a stale owner set. Everything else about the handle, including the
|
|
7192
|
+
* connection and the recovery module, is shared with the original.
|
|
7193
|
+
*/
|
|
7194
|
+
pinned(view) {
|
|
7195
|
+
return new _Vault(this.address, { ...this.ctx, view });
|
|
7196
|
+
}
|
|
7197
|
+
/** Active owners. Prefers a pinned view, then the indexer, then chain. */
|
|
6872
7198
|
async owners() {
|
|
7199
|
+
if (this.ctx.view) return this.ctx.view.owners;
|
|
6873
7200
|
if (await this.useIndexer()) {
|
|
6874
7201
|
try {
|
|
6875
7202
|
const owners = await this.requireQueries("owners").owners(this.address);
|
|
@@ -6877,12 +7204,40 @@ var Vault = class {
|
|
|
6877
7204
|
} catch {
|
|
6878
7205
|
}
|
|
6879
7206
|
}
|
|
7207
|
+
return this.chainOwners();
|
|
7208
|
+
}
|
|
7209
|
+
/**
|
|
7210
|
+
* Owners straight from the chain, bypassing the indexer entirely.
|
|
7211
|
+
*
|
|
7212
|
+
* {@link owners} prefers the indexer, which is right for display and wrong for
|
|
7213
|
+
* anything that gates a write. A lagging indexer would let the SDK build a proposal
|
|
7214
|
+
* against an owner set that no longer exists — admitting a `removeOwner` that drops
|
|
7215
|
+
* the vault below its threshold, or rejecting one that is perfectly valid. Every
|
|
7216
|
+
* propose-time precondition reads through here instead.
|
|
7217
|
+
*/
|
|
7218
|
+
async chainOwners() {
|
|
6880
7219
|
return (await this.contract().getOwners()).map((o) => (0, import_quais14.getAddress)(String(o)));
|
|
6881
7220
|
}
|
|
6882
7221
|
async isOwner(address2) {
|
|
6883
7222
|
return this.contract().isOwner((0, import_quais14.getAddress)(address2));
|
|
6884
7223
|
}
|
|
7224
|
+
/** Approvals required to execute. Prefers a pinned view, then the indexer, then chain. */
|
|
6885
7225
|
async threshold() {
|
|
7226
|
+
if (this.ctx.view) return this.ctx.view.threshold;
|
|
7227
|
+
if (await this.useIndexer()) {
|
|
7228
|
+
try {
|
|
7229
|
+
const wallet = await this.requireQueries("threshold").wallet(this.address);
|
|
7230
|
+
if (wallet) return wallet.threshold;
|
|
7231
|
+
} catch {
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
return this.chainThreshold();
|
|
7235
|
+
}
|
|
7236
|
+
/**
|
|
7237
|
+
* Threshold straight from the chain. The write-path counterpart to {@link threshold},
|
|
7238
|
+
* for the same reason {@link chainOwners} exists.
|
|
7239
|
+
*/
|
|
7240
|
+
async chainThreshold() {
|
|
6886
7241
|
return Number(await this.contract().threshold());
|
|
6887
7242
|
}
|
|
6888
7243
|
async balance() {
|
|
@@ -6935,6 +7290,53 @@ var Vault = class {
|
|
|
6935
7290
|
}
|
|
6936
7291
|
return this.fromChain(hash);
|
|
6937
7292
|
}
|
|
7293
|
+
/**
|
|
7294
|
+
* Several transactions at once, keyed by hash.
|
|
7295
|
+
*
|
|
7296
|
+
* The plural form exists because the singular one is expensive to loop: each
|
|
7297
|
+
* `transaction()` re-reads the owner set, the threshold and the indexer head, so
|
|
7298
|
+
* fetching fifty costs fifty times that. This resolves the whole set the way the
|
|
7299
|
+
* listing methods already do — one query for the rows, one owner/threshold read
|
|
7300
|
+
* shared across them, one batched confirmations query.
|
|
7301
|
+
*
|
|
7302
|
+
* Hashes the indexer does not have fall back to chain reads, bounded by a pool.
|
|
7303
|
+
* Hashes that exist nowhere are simply absent from the map rather than throwing:
|
|
7304
|
+
* one unknown hash should not lose the caller the other forty-nine.
|
|
7305
|
+
*/
|
|
7306
|
+
async transactions(txHashes) {
|
|
7307
|
+
const hashes = [...new Set(txHashes.map((h) => normalizeTxHash(h)))];
|
|
7308
|
+
const found = /* @__PURE__ */ new Map();
|
|
7309
|
+
if (hashes.length === 0) return found;
|
|
7310
|
+
if (await this.useIndexer()) {
|
|
7311
|
+
try {
|
|
7312
|
+
const queries = this.requireQueries("Reading transactions");
|
|
7313
|
+
const [rows, owners, threshold] = await Promise.all([
|
|
7314
|
+
queries.transactionsByHash(this.address, hashes),
|
|
7315
|
+
this.owners(),
|
|
7316
|
+
this.threshold()
|
|
7317
|
+
]);
|
|
7318
|
+
for (const tx of await this.hydrateRows(rows, owners, threshold)) {
|
|
7319
|
+
found.set(tx.hash.toLowerCase(), tx);
|
|
7320
|
+
}
|
|
7321
|
+
} catch {
|
|
7322
|
+
}
|
|
7323
|
+
}
|
|
7324
|
+
const missing = hashes.filter((hash) => !found.has(hash));
|
|
7325
|
+
if (missing.length > 0) {
|
|
7326
|
+
const resolved = await mapPooled(missing, DEFAULT_CONCURRENCY, async (hash) => {
|
|
7327
|
+
try {
|
|
7328
|
+
return await this.fromChain(hash);
|
|
7329
|
+
} catch (err) {
|
|
7330
|
+
if (err instanceof NotFoundError) return null;
|
|
7331
|
+
throw err;
|
|
7332
|
+
}
|
|
7333
|
+
});
|
|
7334
|
+
for (const tx of resolved) {
|
|
7335
|
+
if (tx) found.set(tx.hash.toLowerCase(), tx);
|
|
7336
|
+
}
|
|
7337
|
+
}
|
|
7338
|
+
return found;
|
|
7339
|
+
}
|
|
6938
7340
|
async pendingTransactions(options = {}) {
|
|
6939
7341
|
const queries = this.requireQueries("Listing pending transactions");
|
|
6940
7342
|
const [rows, owners, threshold] = await Promise.all([
|
|
@@ -6965,7 +7367,7 @@ var Vault = class {
|
|
|
6965
7367
|
this.address,
|
|
6966
7368
|
rows.map((r) => r.tx_hash)
|
|
6967
7369
|
);
|
|
6968
|
-
const indexedAtBlock =
|
|
7370
|
+
const indexedAtBlock = await this.indexerHead();
|
|
6969
7371
|
return rows.map((row) => {
|
|
6970
7372
|
const confirmed = (confirmations.get(row.tx_hash.toLowerCase()) ?? []).map(
|
|
6971
7373
|
(c) => c.owner_address
|
|
@@ -6988,7 +7390,7 @@ var Vault = class {
|
|
|
6988
7390
|
]);
|
|
6989
7391
|
if (!row) return null;
|
|
6990
7392
|
const confirmations = await queries.confirmations(this.address, hash);
|
|
6991
|
-
const indexedAtBlock =
|
|
7393
|
+
const indexedAtBlock = await this.indexerHead();
|
|
6992
7394
|
return this.buildTransaction({
|
|
6993
7395
|
row,
|
|
6994
7396
|
confirmedBy: confirmations.filter((c) => c.is_active).map((c) => c.owner_address),
|
|
@@ -7051,7 +7453,10 @@ var Vault = class {
|
|
|
7051
7453
|
value,
|
|
7052
7454
|
data,
|
|
7053
7455
|
proposer: (0, import_quais14.getAddress)(row.submitted_by),
|
|
7054
|
-
|
|
7456
|
+
// The indexer records the block, not the chain timestamp — they are not
|
|
7457
|
+
// interchangeable, and reporting a block number as `proposedAt` renders as 1970.
|
|
7458
|
+
proposedAt: 0,
|
|
7459
|
+
proposedAtBlock: toNumber(row.submitted_at_block),
|
|
7055
7460
|
kind: decodeResult.kind,
|
|
7056
7461
|
decoded: decodeResult.decoded,
|
|
7057
7462
|
summary: decodeResult.summary,
|
|
@@ -7142,6 +7547,18 @@ var Vault = class {
|
|
|
7142
7547
|
*/
|
|
7143
7548
|
async affordances(txHash, caller) {
|
|
7144
7549
|
const hash = normalizeTxHash(txHash);
|
|
7550
|
+
return this.resolveAffordances(hash, this.transaction(hash), caller);
|
|
7551
|
+
}
|
|
7552
|
+
/**
|
|
7553
|
+
* Shared body of {@link affordances}, taking the transaction either as a value or as
|
|
7554
|
+
* an in-flight promise.
|
|
7555
|
+
*
|
|
7556
|
+
* The promise form is the point: a caller that already holds the record (like
|
|
7557
|
+
* {@link describe}) passes it and skips a redundant fetch, while `affordances()`
|
|
7558
|
+
* passes the unawaited promise so the transaction read still overlaps the two
|
|
7559
|
+
* ownership probes rather than serialising behind them.
|
|
7560
|
+
*/
|
|
7561
|
+
async resolveAffordances(hash, transaction, caller) {
|
|
7145
7562
|
const who = caller ?? await this.ctx.connection.addressOrNull();
|
|
7146
7563
|
if (!who) {
|
|
7147
7564
|
throw new ValidationError(
|
|
@@ -7150,7 +7567,7 @@ var Vault = class {
|
|
|
7150
7567
|
}
|
|
7151
7568
|
const vault2 = this.contract();
|
|
7152
7569
|
const [tx, isOwner, hasApproved] = await Promise.all([
|
|
7153
|
-
|
|
7570
|
+
transaction,
|
|
7154
7571
|
vault2.isOwner((0, import_quais14.getAddress)(who)),
|
|
7155
7572
|
vault2.hasApproved(hash, (0, import_quais14.getAddress)(who))
|
|
7156
7573
|
]);
|
|
@@ -7245,7 +7662,7 @@ var Vault = class {
|
|
|
7245
7662
|
// --- self-calls -------------------------------------------------------
|
|
7246
7663
|
addOwner: async (owner, options = {}) => this.proposeSelfCall(selfCall.addOwner(assertQuaiAddress(owner, "owner")), options),
|
|
7247
7664
|
removeOwner: async (owner, options = {}) => {
|
|
7248
|
-
const [owners, threshold] = await Promise.all([this.
|
|
7665
|
+
const [owners, threshold] = await Promise.all([this.chainOwners(), this.chainThreshold()]);
|
|
7249
7666
|
if (!owners.some((o) => o.toLowerCase() === owner.toLowerCase())) {
|
|
7250
7667
|
throw new PreconditionError(`${owner} is not an owner of this vault.`);
|
|
7251
7668
|
}
|
|
@@ -7258,7 +7675,7 @@ var Vault = class {
|
|
|
7258
7675
|
return this.proposeSelfCall(selfCall.removeOwner((0, import_quais14.getAddress)(owner)), options);
|
|
7259
7676
|
},
|
|
7260
7677
|
changeThreshold: async (threshold, options = {}) => {
|
|
7261
|
-
const owners = await this.
|
|
7678
|
+
const owners = await this.chainOwners();
|
|
7262
7679
|
if (threshold < 1 || threshold > owners.length) {
|
|
7263
7680
|
throw new ValidationError(
|
|
7264
7681
|
`Invalid threshold ${threshold}: this vault has ${owners.length} owners.`
|
|
@@ -7692,9 +8109,7 @@ var Vault = class {
|
|
|
7692
8109
|
const pollIntervalMs = options.pollIntervalMs ?? 15e3;
|
|
7693
8110
|
const deadline = Date.now() + timeoutMs;
|
|
7694
8111
|
for (; ; ) {
|
|
7695
|
-
if (options.signal?.aborted)
|
|
7696
|
-
throw new PreconditionError("Waiting for the transaction was aborted.");
|
|
7697
|
-
}
|
|
8112
|
+
if (options.signal?.aborted) throw new AbortError("Waiting for the transaction");
|
|
7698
8113
|
const tx = await this.fromChain(hash);
|
|
7699
8114
|
options.onPoll?.(tx);
|
|
7700
8115
|
if (tx.status === "ready") return tx;
|
|
@@ -7751,7 +8166,7 @@ var Vault = class {
|
|
|
7751
8166
|
if (tx.decodedRevert) lines.push(` revert: ${tx.decodedRevert.message}`);
|
|
7752
8167
|
lines.push(` source: ${tx.source}`);
|
|
7753
8168
|
if (caller || this.ctx.connection.hasSigner()) {
|
|
7754
|
-
const affordances = await this.
|
|
8169
|
+
const affordances = await this.resolveAffordances(tx.hash, tx, caller);
|
|
7755
8170
|
const allowed = affordances.filter((a) => a.allowed).map((a) => a.action);
|
|
7756
8171
|
lines.push(` you can: ${allowed.length > 0 ? allowed.join(", ") : "nothing right now"}`);
|
|
7757
8172
|
}
|