@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.js
CHANGED
|
@@ -44,6 +44,11 @@ var QuaiVaultError = class extends Error {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
+
var AbortError = class extends QuaiVaultError {
|
|
48
|
+
constructor(operation = "The operation") {
|
|
49
|
+
super("ABORTED", `${operation} was aborted.`);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
47
52
|
var ConfigError = class extends QuaiVaultError {
|
|
48
53
|
constructor(message, remediation) {
|
|
49
54
|
super("CONFIG", message, { remediation });
|
|
@@ -162,6 +167,16 @@ var Connection = class {
|
|
|
162
167
|
write ? this.requireSigner("This recovery write") : this.provider
|
|
163
168
|
);
|
|
164
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Read-only handle to a token contract.
|
|
172
|
+
*
|
|
173
|
+
* No write variant: the SDK never calls a token directly. Moving tokens goes
|
|
174
|
+
* through the vault's proposal flow, which encodes the transfer as calldata.
|
|
175
|
+
*/
|
|
176
|
+
token(address2, standard) {
|
|
177
|
+
const abi = standard === "ERC20" ? Erc20Abi : standard === "ERC721" ? Erc721Abi : Erc1155Abi;
|
|
178
|
+
return new Contract(address2, abi, this.provider);
|
|
179
|
+
}
|
|
165
180
|
};
|
|
166
181
|
function createPrivateKeySigner(privateKey, provider) {
|
|
167
182
|
const normalized = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
|
|
@@ -398,7 +413,9 @@ function resolveConfig(options = {}) {
|
|
|
398
413
|
contracts,
|
|
399
414
|
indexer,
|
|
400
415
|
rpcUrl,
|
|
401
|
-
|
|
416
|
+
// An explicit signer wins outright in `Connection`, so resolving a key here would
|
|
417
|
+
// only pull a secret into memory that nothing can use. Skip it.
|
|
418
|
+
privateKey: options.signer ? void 0 : pick(options.privateKey, env[ENV_VARS.privateKey]),
|
|
402
419
|
consistency,
|
|
403
420
|
maxIndexerLagBlocks: options.maxIndexerLagBlocks ?? parseLag(env[ENV_VARS.maxIndexerLagBlocks]) ?? 50,
|
|
404
421
|
retry: {
|
|
@@ -528,7 +545,7 @@ function isTransient(error) {
|
|
|
528
545
|
}
|
|
529
546
|
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
530
547
|
if (signal?.aborted) {
|
|
531
|
-
reject(new
|
|
548
|
+
reject(new AbortError("Waiting to retry"));
|
|
532
549
|
return;
|
|
533
550
|
}
|
|
534
551
|
const timer = setTimeout(() => {
|
|
@@ -537,7 +554,7 @@ var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
537
554
|
}, ms);
|
|
538
555
|
const onAbort = () => {
|
|
539
556
|
clearTimeout(timer);
|
|
540
|
-
reject(new
|
|
557
|
+
reject(new AbortError("Waiting to retry"));
|
|
541
558
|
};
|
|
542
559
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
543
560
|
});
|
|
@@ -547,7 +564,7 @@ async function withRetry(fn, options = {}) {
|
|
|
547
564
|
const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;
|
|
548
565
|
let lastError;
|
|
549
566
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
550
|
-
if (options.signal?.aborted) throw new
|
|
567
|
+
if (options.signal?.aborted) throw new AbortError("The retried operation");
|
|
551
568
|
try {
|
|
552
569
|
return await fn();
|
|
553
570
|
} catch (error) {
|
|
@@ -742,8 +759,39 @@ var FactoryContract = class {
|
|
|
742
759
|
|
|
743
760
|
// src/errors/decode.ts
|
|
744
761
|
import { Interface, AbiCoder } from "quais";
|
|
762
|
+
|
|
763
|
+
// src/text.ts
|
|
764
|
+
var UNSAFE_CHARS = new RegExp(
|
|
765
|
+
[
|
|
766
|
+
"[\\u0000-\\u001F",
|
|
767
|
+
// C0 controls, incl. ESC (0x1B) — the head of every ANSI sequence
|
|
768
|
+
"\\u007F-\\u009F",
|
|
769
|
+
// DEL and the C1 controls
|
|
770
|
+
"\\u200B-\\u200F",
|
|
771
|
+
// zero-width space/joiners, LRM, RLM
|
|
772
|
+
"\\u202A-\\u202E",
|
|
773
|
+
// bidi embeddings and overrides
|
|
774
|
+
"\\u2060-\\u2064",
|
|
775
|
+
// word joiner and the invisible operators
|
|
776
|
+
"\\u2066-\\u2069",
|
|
777
|
+
// bidi isolates
|
|
778
|
+
"\\uFEFF]"
|
|
779
|
+
// zero-width no-break space / BOM
|
|
780
|
+
].join(""),
|
|
781
|
+
"g"
|
|
782
|
+
);
|
|
783
|
+
var DEFAULT_TEXT_LIMIT = 128;
|
|
784
|
+
function sanitizeText(value, maxLength = DEFAULT_TEXT_LIMIT) {
|
|
785
|
+
if (typeof value !== "string") return "";
|
|
786
|
+
const cleaned = value.replace(UNSAFE_CHARS, "").trim();
|
|
787
|
+
if (cleaned.length <= maxLength) return cleaned;
|
|
788
|
+
return `${cleaned.slice(0, Math.max(0, maxLength - 1))}\u2026`;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/errors/decode.ts
|
|
745
792
|
var SOLIDITY_ERROR_SELECTOR = "0x08c379a0";
|
|
746
793
|
var SOLIDITY_PANIC_SELECTOR = "0x4e487b71";
|
|
794
|
+
var REVERT_TEXT_LIMIT = 256;
|
|
747
795
|
var registry = null;
|
|
748
796
|
function buildRegistry() {
|
|
749
797
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -833,7 +881,7 @@ function formatArgs(fragment, args) {
|
|
|
833
881
|
if (args.length === 0) return "";
|
|
834
882
|
const parts = fragment.inputs.map((input, i) => {
|
|
835
883
|
const value = args[i];
|
|
836
|
-
const rendered = typeof value === "bigint" ? value.toString() : String(value);
|
|
884
|
+
const rendered = typeof value === "bigint" ? value.toString() : typeof value === "string" ? sanitizeText(value, REVERT_TEXT_LIMIT) : String(value);
|
|
837
885
|
return input.name ? `${input.name}: ${rendered}` : rendered;
|
|
838
886
|
});
|
|
839
887
|
return `(${parts.join(", ")})`;
|
|
@@ -849,7 +897,7 @@ function decodeRevert(data) {
|
|
|
849
897
|
name: "Error",
|
|
850
898
|
args: [reason],
|
|
851
899
|
selector,
|
|
852
|
-
message:
|
|
900
|
+
message: sanitizeText(reason, REVERT_TEXT_LIMIT)
|
|
853
901
|
};
|
|
854
902
|
} catch {
|
|
855
903
|
return void 0;
|
|
@@ -916,7 +964,7 @@ function knownErrorSelectors() {
|
|
|
916
964
|
}
|
|
917
965
|
|
|
918
966
|
// src/encode/index.ts
|
|
919
|
-
import { AbiCoder as AbiCoder2, Interface as Interface2, concat, getBytes, solidityPacked
|
|
967
|
+
import { AbiCoder as AbiCoder2, Interface as Interface2, concat, getBytes, solidityPacked } from "quais";
|
|
920
968
|
|
|
921
969
|
// src/types.ts
|
|
922
970
|
var Operation = /* @__PURE__ */ ((Operation2) => {
|
|
@@ -937,6 +985,7 @@ var MAX_EXECUTION_DELAY = 30 * 24 * 60 * 60;
|
|
|
937
985
|
var MAX_OWNERS = 20;
|
|
938
986
|
var MAX_MODULES = 50;
|
|
939
987
|
var SENTINEL_MODULES = "0x0000000000000000000000000000000000000001";
|
|
988
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
940
989
|
var selfCall = {
|
|
941
990
|
addOwner: (owner) => vault.encodeFunctionData("addOwner", [owner]),
|
|
942
991
|
removeOwner: (owner) => vault.encodeFunctionData("removeOwner", [owner]),
|
|
@@ -1104,7 +1153,7 @@ var syncStrategy = {
|
|
|
1104
1153
|
async mine({ factory, deployer, bytecodeHash, targetPrefix, maxAttempts, timeoutMs, onProgress, signal }) {
|
|
1105
1154
|
const startedAt = Date.now();
|
|
1106
1155
|
for (let attempts = 0; attempts < maxAttempts; ) {
|
|
1107
|
-
if (signal?.aborted) throw new
|
|
1156
|
+
if (signal?.aborted) throw new AbortError("Salt mining");
|
|
1108
1157
|
if (Date.now() - startedAt > timeoutMs) {
|
|
1109
1158
|
throw new SaltMiningError(
|
|
1110
1159
|
`Salt mining timed out after ${timeoutMs} ms (${attempts} attempts).`,
|
|
@@ -1132,109 +1181,127 @@ var syncStrategy = {
|
|
|
1132
1181
|
);
|
|
1133
1182
|
}
|
|
1134
1183
|
};
|
|
1135
|
-
var
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1184
|
+
var WorkersUnavailable = class extends Error {
|
|
1185
|
+
};
|
|
1186
|
+
async function loadNodeWorkers() {
|
|
1187
|
+
try {
|
|
1188
|
+
const { Worker } = await import("worker_threads");
|
|
1189
|
+
const os = await import("os");
|
|
1190
|
+
return { Worker, parallelism: (os.availableParallelism ?? (() => os.cpus().length))() };
|
|
1191
|
+
} catch {
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function createWorkerThreadsStrategy(load = loadNodeWorkers) {
|
|
1196
|
+
return {
|
|
1197
|
+
name: "worker_threads",
|
|
1198
|
+
async mine(job) {
|
|
1199
|
+
const runtime = await load();
|
|
1200
|
+
if (!runtime) return syncStrategy.mine(job);
|
|
1201
|
+
try {
|
|
1202
|
+
return await mineInWorkers(runtime.Worker, runtime.parallelism, job);
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
if (err instanceof WorkersUnavailable) return syncStrategy.mine(job);
|
|
1205
|
+
throw err;
|
|
1206
|
+
}
|
|
1146
1207
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
var workerThreadsStrategy = createWorkerThreadsStrategy();
|
|
1211
|
+
function mineInWorkers(Worker, parallelism, job) {
|
|
1212
|
+
const { factory, deployer, bytecodeHash, targetPrefix, maxAttempts, timeoutMs, onProgress, signal } = job;
|
|
1213
|
+
const workerCount = Math.max(1, Math.min(8, parallelism - 1));
|
|
1214
|
+
const perWorker = Math.ceil(maxAttempts / workerCount);
|
|
1215
|
+
const startedAt = Date.now();
|
|
1216
|
+
const workerSource = `
|
|
1217
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
1218
|
+
const { getCreate2Address, hexlify, isQuaiAddress, keccak256, randomBytes, solidityPacked } = require('quais');
|
|
1219
|
+
const { factory, deployer, bytecodeHash, targetPrefix, maxAttempts } = workerData;
|
|
1220
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1221
|
+
const salt = hexlify(randomBytes(32));
|
|
1222
|
+
const fullSalt = keccak256(solidityPacked(['address', 'bytes32'], [deployer, salt]));
|
|
1223
|
+
const address = getCreate2Address(factory, fullSalt, bytecodeHash);
|
|
1224
|
+
if (address.toLowerCase().startsWith(targetPrefix) && isQuaiAddress(address)) {
|
|
1225
|
+
parentPort.postMessage({ type: 'result', salt, address, attempts: i + 1 });
|
|
1226
|
+
return;
|
|
1164
1227
|
}
|
|
1165
|
-
parentPort.postMessage({ type: '
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
)
|
|
1228
|
+
if ((i + 1) % ${PROGRESS_INTERVAL} === 0) parentPort.postMessage({ type: 'progress', attempts: i + 1 });
|
|
1229
|
+
}
|
|
1230
|
+
parentPort.postMessage({ type: 'exhausted', attempts: maxAttempts });
|
|
1231
|
+
`;
|
|
1232
|
+
return new Promise((resolve, reject) => {
|
|
1233
|
+
const workers = [];
|
|
1234
|
+
let settled = false;
|
|
1235
|
+
let exhausted = 0;
|
|
1236
|
+
let totalAttempts = 0;
|
|
1237
|
+
const progressByWorker = new Array(workerCount).fill(0);
|
|
1238
|
+
const cleanup = () => {
|
|
1239
|
+
clearTimeout(timer);
|
|
1240
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1241
|
+
for (const w of workers) void w.terminate();
|
|
1242
|
+
};
|
|
1243
|
+
const settle = (fn) => {
|
|
1244
|
+
if (settled) return;
|
|
1245
|
+
settled = true;
|
|
1246
|
+
cleanup();
|
|
1247
|
+
fn();
|
|
1248
|
+
};
|
|
1249
|
+
const timer = setTimeout(
|
|
1250
|
+
() => settle(
|
|
1251
|
+
() => reject(
|
|
1252
|
+
new SaltMiningError(
|
|
1253
|
+
`Salt mining timed out after ${timeoutMs} ms (~${totalAttempts} attempts across ${workerCount} workers).`,
|
|
1254
|
+
"Raise timeoutMs or maxAttempts."
|
|
1191
1255
|
)
|
|
1192
|
-
)
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1256
|
+
)
|
|
1257
|
+
),
|
|
1258
|
+
timeoutMs
|
|
1259
|
+
);
|
|
1260
|
+
const onAbort = () => settle(() => reject(new AbortError("Salt mining")));
|
|
1261
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1262
|
+
for (let i = 0; i < workerCount; i++) {
|
|
1263
|
+
let worker;
|
|
1264
|
+
try {
|
|
1265
|
+
worker = new Worker(workerSource, {
|
|
1199
1266
|
eval: true,
|
|
1200
1267
|
workerData: { factory, deployer, bytecodeHash, targetPrefix, maxAttempts: perWorker }
|
|
1201
1268
|
});
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
settle(() => reject(new WorkersUnavailable(String(err))));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
workers.push(worker);
|
|
1274
|
+
worker.on("message", (msg) => {
|
|
1275
|
+
if (msg.type === "progress") {
|
|
1276
|
+
progressByWorker[i] = msg.attempts ?? 0;
|
|
1277
|
+
totalAttempts = progressByWorker.reduce((a, b) => a + b, 0);
|
|
1278
|
+
onProgress?.(totalAttempts);
|
|
1279
|
+
} else if (msg.type === "result") {
|
|
1280
|
+
settle(
|
|
1281
|
+
() => resolve({
|
|
1282
|
+
salt: msg.salt,
|
|
1283
|
+
predictedAddress: msg.address,
|
|
1284
|
+
attempts: totalAttempts + (msg.attempts ?? 0),
|
|
1285
|
+
durationMs: Date.now() - startedAt
|
|
1286
|
+
})
|
|
1287
|
+
);
|
|
1288
|
+
} else if (msg.type === "exhausted") {
|
|
1289
|
+
if (++exhausted === workerCount) {
|
|
1209
1290
|
settle(
|
|
1210
|
-
() =>
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
durationMs: Date.now() - startedAt
|
|
1215
|
-
})
|
|
1216
|
-
);
|
|
1217
|
-
} else if (msg.type === "exhausted") {
|
|
1218
|
-
if (++exhausted === workerCount) {
|
|
1219
|
-
settle(
|
|
1220
|
-
() => reject(
|
|
1221
|
-
new SaltMiningError(
|
|
1222
|
-
`No valid salt found for prefix ${targetPrefix} after ${maxAttempts} attempts.`,
|
|
1223
|
-
"Raise maxAttempts, or confirm the deployer address is on the shard you expect."
|
|
1224
|
-
)
|
|
1291
|
+
() => reject(
|
|
1292
|
+
new SaltMiningError(
|
|
1293
|
+
`No valid salt found for prefix ${targetPrefix} after ${maxAttempts} attempts.`,
|
|
1294
|
+
"Raise maxAttempts, or confirm the deployer address is on the shard you expect."
|
|
1225
1295
|
)
|
|
1226
|
-
)
|
|
1227
|
-
|
|
1296
|
+
)
|
|
1297
|
+
);
|
|
1228
1298
|
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
worker.on("error", (err) => settle(() => reject(new WorkersUnavailable(err.message))));
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1238
1305
|
function defaultStrategy() {
|
|
1239
1306
|
const hasNode = typeof globalThis.process?.versions?.node === "string";
|
|
1240
1307
|
return hasNode ? workerThreadsStrategy : syncStrategy;
|
|
@@ -1456,7 +1523,7 @@ function validateCreateParams(params) {
|
|
|
1456
1523
|
owners.forEach((owner, i) => {
|
|
1457
1524
|
assertQuaiAddress(owner, `owner[${i}]`);
|
|
1458
1525
|
const key = owner.toLowerCase();
|
|
1459
|
-
if (key ===
|
|
1526
|
+
if (key === ZERO_ADDRESS || key === SENTINEL_MODULES) {
|
|
1460
1527
|
throw new ValidationError(
|
|
1461
1528
|
`Owner ${owner} is reserved \u2014 the zero address and the module sentinel 0x\u202601 are rejected.`
|
|
1462
1529
|
);
|
|
@@ -1465,6 +1532,11 @@ function validateCreateParams(params) {
|
|
|
1465
1532
|
seen.add(key);
|
|
1466
1533
|
});
|
|
1467
1534
|
if (params.initialModules?.length) {
|
|
1535
|
+
if (params.initialModules.length > MAX_MODULES) {
|
|
1536
|
+
throw new ValidationError(
|
|
1537
|
+
`A vault may have at most ${MAX_MODULES} modules (got ${params.initialModules.length}).`
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1468
1540
|
assertQuaiAddresses(params.initialModules, "initialModules");
|
|
1469
1541
|
}
|
|
1470
1542
|
if (params.initialDelegatecallTargets?.length) {
|
|
@@ -1502,7 +1574,8 @@ function extractCreatedVault(receipt, factoryAddress) {
|
|
|
1502
1574
|
}
|
|
1503
1575
|
|
|
1504
1576
|
// src/indexer/client.ts
|
|
1505
|
-
import {
|
|
1577
|
+
import { PostgrestClient } from "@supabase/postgrest-js";
|
|
1578
|
+
import { RealtimeClient } from "@supabase/realtime-js";
|
|
1506
1579
|
|
|
1507
1580
|
// src/indexer/schemas.ts
|
|
1508
1581
|
var schemas_exports = {};
|
|
@@ -1701,10 +1774,11 @@ function toNumber(value, fallback = 0) {
|
|
|
1701
1774
|
}
|
|
1702
1775
|
|
|
1703
1776
|
// src/indexer/client.ts
|
|
1704
|
-
var SDK_VERSION = "0.
|
|
1777
|
+
var SDK_VERSION = true ? "0.2.0" : "dev";
|
|
1705
1778
|
var IndexerClient = class {
|
|
1706
1779
|
config;
|
|
1707
1780
|
client;
|
|
1781
|
+
realtime = null;
|
|
1708
1782
|
healthCache = null;
|
|
1709
1783
|
inflightHealth = null;
|
|
1710
1784
|
/** Health results are reused for this long to keep read paths cheap. */
|
|
@@ -1712,19 +1786,46 @@ var IndexerClient = class {
|
|
|
1712
1786
|
constructor(config, options = {}) {
|
|
1713
1787
|
this.config = config;
|
|
1714
1788
|
this.healthCacheMs = options.healthCacheMs ?? 5e3;
|
|
1715
|
-
this.client =
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1789
|
+
this.client = new PostgrestClient(`${baseUrl(config.url)}/rest/v1`, {
|
|
1790
|
+
headers: {
|
|
1791
|
+
apikey: config.anonKey,
|
|
1792
|
+
Authorization: `Bearer ${config.anonKey}`,
|
|
1793
|
+
"x-client-info": `quaivault-sdk/${SDK_VERSION}`
|
|
1794
|
+
},
|
|
1795
|
+
schema: config.schema
|
|
1719
1796
|
});
|
|
1720
1797
|
}
|
|
1721
|
-
/**
|
|
1722
|
-
get
|
|
1798
|
+
/** The PostgREST client, for queries the SDK does not wrap. */
|
|
1799
|
+
get rest() {
|
|
1723
1800
|
return this.client;
|
|
1724
1801
|
}
|
|
1725
1802
|
from(table) {
|
|
1726
1803
|
return this.client.from(table);
|
|
1727
1804
|
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Realtime client, constructed on first use.
|
|
1807
|
+
*
|
|
1808
|
+
* Lazy because it opens a WebSocket and starts a heartbeat, and the large majority
|
|
1809
|
+
* of SDK usage never calls `watch()`. Nothing here should cost a socket unless
|
|
1810
|
+
* somebody asked to follow a vault.
|
|
1811
|
+
*/
|
|
1812
|
+
realtimeClient() {
|
|
1813
|
+
if (!this.realtime) {
|
|
1814
|
+
this.realtime = new RealtimeClient(
|
|
1815
|
+
`${baseUrl(this.config.url).replace(/^http/, "ws")}/realtime/v1`,
|
|
1816
|
+
{ params: { apikey: this.config.anonKey } }
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
return this.realtime;
|
|
1820
|
+
}
|
|
1821
|
+
/** Open a Realtime channel. See `watchVault`, which is what callers should use. */
|
|
1822
|
+
channel(name) {
|
|
1823
|
+
return this.realtimeClient().channel(name);
|
|
1824
|
+
}
|
|
1825
|
+
/** Close a channel opened by {@link channel}. */
|
|
1826
|
+
async removeChannel(channel) {
|
|
1827
|
+
await this.realtimeClient().removeChannel(channel);
|
|
1828
|
+
}
|
|
1728
1829
|
/**
|
|
1729
1830
|
* Run a query, parse each row, and surface failures as `IndexerQueryError`.
|
|
1730
1831
|
*
|
|
@@ -1742,6 +1843,36 @@ var IndexerClient = class {
|
|
|
1742
1843
|
}
|
|
1743
1844
|
return (data ?? []).map((row) => schema.parse(row));
|
|
1744
1845
|
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Paged select: an exact `hasMore`, an approximate `total`.
|
|
1848
|
+
*
|
|
1849
|
+
* Both halves of that are deliberate. An `exact` count makes Postgres scan every
|
|
1850
|
+
* matching row on every page request, which is invisible on a young vault and
|
|
1851
|
+
* quadratic on a busy one — so the count is `estimated`, cheap and good enough for
|
|
1852
|
+
* "roughly how much history is there".
|
|
1853
|
+
*
|
|
1854
|
+
* But `hasMore` derived from an estimate would be a lie a caller can act on: a
|
|
1855
|
+
* paging loop that stops early drops real rows. So callers request one row beyond
|
|
1856
|
+
* the page and this trims it, which answers "is there another page" exactly, for
|
|
1857
|
+
* the cost of a single extra row.
|
|
1858
|
+
*/
|
|
1859
|
+
async selectPage(table, schema, limit, build) {
|
|
1860
|
+
const { data, error, count } = await build(this.client.from(table));
|
|
1861
|
+
if (error) {
|
|
1862
|
+
throw new IndexerQueryError(`Indexer query on "${table}" failed: ${error.message}`, error);
|
|
1863
|
+
}
|
|
1864
|
+
const rows = data ?? [];
|
|
1865
|
+
const hasMore = rows.length > limit;
|
|
1866
|
+
const parsed = (hasMore ? rows.slice(0, limit) : rows).map(
|
|
1867
|
+
(row) => schema.parse(row)
|
|
1868
|
+
);
|
|
1869
|
+
return {
|
|
1870
|
+
data: parsed,
|
|
1871
|
+
// Never report a total below what was just handed out, however the estimate lands.
|
|
1872
|
+
total: Math.max(count ?? 0, parsed.length),
|
|
1873
|
+
hasMore
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1745
1876
|
/** Single-row variant. Returns null for PostgREST's "no rows" code. */
|
|
1746
1877
|
async selectOne(table, schema, build) {
|
|
1747
1878
|
const { data, error } = await build(this.client.from(table));
|
|
@@ -1796,7 +1927,7 @@ var IndexerClient = class {
|
|
|
1796
1927
|
const pollIntervalMs = options.pollIntervalMs ?? 1500;
|
|
1797
1928
|
const deadline = Date.now() + timeoutMs;
|
|
1798
1929
|
for (; ; ) {
|
|
1799
|
-
if (options.signal?.aborted) throw new
|
|
1930
|
+
if (options.signal?.aborted) throw new AbortError("Waiting for the indexer");
|
|
1800
1931
|
const state = await this.state();
|
|
1801
1932
|
const head = state?.lastIndexedBlock ?? 0;
|
|
1802
1933
|
if (head >= blockNumber) return { reached: true, lastIndexedBlock: head };
|
|
@@ -1808,12 +1939,12 @@ var IndexerClient = class {
|
|
|
1808
1939
|
}
|
|
1809
1940
|
async probeHealth() {
|
|
1810
1941
|
if (this.config.healthUrl) {
|
|
1942
|
+
const controller = new AbortController();
|
|
1943
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1811
1944
|
try {
|
|
1812
|
-
const controller = new AbortController();
|
|
1813
|
-
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1814
1945
|
const res = await fetch(new URL("/health", this.config.healthUrl), {
|
|
1815
1946
|
signal: controller.signal
|
|
1816
|
-
})
|
|
1947
|
+
});
|
|
1817
1948
|
const body = await res.json();
|
|
1818
1949
|
const d = body.details ?? {};
|
|
1819
1950
|
return {
|
|
@@ -1825,6 +1956,8 @@ var IndexerClient = class {
|
|
|
1825
1956
|
...res.ok ? {} : { error: `health endpoint returned ${res.status}` }
|
|
1826
1957
|
};
|
|
1827
1958
|
} catch {
|
|
1959
|
+
} finally {
|
|
1960
|
+
clearTimeout(timer);
|
|
1828
1961
|
}
|
|
1829
1962
|
}
|
|
1830
1963
|
try {
|
|
@@ -1847,10 +1980,14 @@ var IndexerClient = class {
|
|
|
1847
1980
|
}
|
|
1848
1981
|
}
|
|
1849
1982
|
};
|
|
1983
|
+
function baseUrl(url) {
|
|
1984
|
+
return url.replace(/\/+$/, "");
|
|
1985
|
+
}
|
|
1850
1986
|
|
|
1851
1987
|
// src/indexer/queries.ts
|
|
1852
1988
|
var DEFAULT_LIMIT = 50;
|
|
1853
1989
|
var MAX_LIMIT = 200;
|
|
1990
|
+
var CONFIRMATION_CHUNK = 40;
|
|
1854
1991
|
function lower(address2) {
|
|
1855
1992
|
return address2.toLowerCase();
|
|
1856
1993
|
}
|
|
@@ -1909,14 +2046,40 @@ var IndexerQueries = class {
|
|
|
1909
2046
|
(q) => q.select("*").eq("wallet_address", lower(vault2)).eq("tx_hash", txHash.toLowerCase()).single()
|
|
1910
2047
|
);
|
|
1911
2048
|
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Several transactions by hash, chunked for the same reasons as
|
|
2051
|
+
* {@link activeConfirmationsBatch} — hashes are long, and they go in the query string.
|
|
2052
|
+
*
|
|
2053
|
+
* Rows come back in no particular order and hashes with no row are simply absent;
|
|
2054
|
+
* the caller matches them up.
|
|
2055
|
+
*/
|
|
2056
|
+
async transactionsByHash(vault2, txHashes) {
|
|
2057
|
+
if (txHashes.length === 0) return [];
|
|
2058
|
+
const hashes = txHashes.map((h) => h.toLowerCase());
|
|
2059
|
+
const chunks = [];
|
|
2060
|
+
for (let i = 0; i < hashes.length; i += CONFIRMATION_CHUNK) {
|
|
2061
|
+
chunks.push(hashes.slice(i, i + CONFIRMATION_CHUNK));
|
|
2062
|
+
}
|
|
2063
|
+
const pages = await Promise.all(
|
|
2064
|
+
chunks.map(
|
|
2065
|
+
(chunk) => this.client.select(
|
|
2066
|
+
"transactions",
|
|
2067
|
+
TransactionSchema,
|
|
2068
|
+
(q) => q.select("*").eq("wallet_address", lower(vault2)).in("tx_hash", chunk)
|
|
2069
|
+
)
|
|
2070
|
+
)
|
|
2071
|
+
);
|
|
2072
|
+
return pages.flat();
|
|
2073
|
+
}
|
|
1912
2074
|
async transactionHistory(vault2, options = {}) {
|
|
1913
2075
|
const { limit, offset } = bounds(options);
|
|
1914
2076
|
const statuses = options.status ?? ["executed", "cancelled", "expired", "failed"];
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2077
|
+
return this.client.selectPage(
|
|
2078
|
+
"transactions",
|
|
2079
|
+
TransactionSchema,
|
|
2080
|
+
limit,
|
|
2081
|
+
(q) => q.select("*", { count: "estimated" }).eq("wallet_address", lower(vault2)).in("status", statuses).order("submitted_at_block", { ascending: false }).range(offset, offset + limit)
|
|
2082
|
+
);
|
|
1920
2083
|
}
|
|
1921
2084
|
/** All confirmations for one transaction, including revoked ones. */
|
|
1922
2085
|
async confirmations(vault2, txHash) {
|
|
@@ -1927,26 +2090,41 @@ var IndexerQueries = class {
|
|
|
1927
2090
|
);
|
|
1928
2091
|
}
|
|
1929
2092
|
/**
|
|
1930
|
-
* Active confirmations for many transactions in
|
|
2093
|
+
* Active confirmations for many transactions, in as few round trips as is safe.
|
|
1931
2094
|
*
|
|
1932
2095
|
* "Active" here means not revoked. It does NOT mean the confirming address is
|
|
1933
2096
|
* still an owner — the indexer does not deactivate confirmations when an owner is
|
|
1934
2097
|
* removed, while the contract invalidates them via approval epochs. Callers must
|
|
1935
2098
|
* intersect with the active owner set; `Vault` does this for you.
|
|
2099
|
+
*
|
|
2100
|
+
* Chunked rather than issued as one `in(...)`, because a single filter fails two
|
|
2101
|
+
* ways at scale. PostgREST reads filters from the query string, and each 32-byte
|
|
2102
|
+
* hash costs ~70 characters there — a full {@link MAX_LIMIT} page of hashes builds a
|
|
2103
|
+
* ~14 KB request line, past the 8 KB cap most reverse proxies apply by default. The
|
|
2104
|
+
* response is bounded too: {@link CONFIRMATION_CHUNK} transactions can carry at most
|
|
2105
|
+
* `chunk × MAX_OWNERS` rows, and the chunk is sized to keep that under the
|
|
2106
|
+
* `max-rows` ceiling PostgREST deployments commonly set, so a busy vault cannot be
|
|
2107
|
+
* silently short-served.
|
|
1936
2108
|
*/
|
|
1937
2109
|
async activeConfirmationsBatch(vault2, txHashes) {
|
|
1938
2110
|
const result = /* @__PURE__ */ new Map();
|
|
1939
2111
|
for (const hash of txHashes) result.set(hash.toLowerCase(), []);
|
|
1940
2112
|
if (txHashes.length === 0) return result;
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2113
|
+
const hashes = txHashes.map((h) => h.toLowerCase());
|
|
2114
|
+
const chunks = [];
|
|
2115
|
+
for (let i = 0; i < hashes.length; i += CONFIRMATION_CHUNK) {
|
|
2116
|
+
chunks.push(hashes.slice(i, i + CONFIRMATION_CHUNK));
|
|
2117
|
+
}
|
|
2118
|
+
const pages = await Promise.all(
|
|
2119
|
+
chunks.map(
|
|
2120
|
+
(chunk) => this.client.select(
|
|
2121
|
+
"confirmations",
|
|
2122
|
+
ConfirmationSchema,
|
|
2123
|
+
(q) => q.select("*").eq("wallet_address", lower(vault2)).in("tx_hash", chunk).eq("is_active", true)
|
|
2124
|
+
)
|
|
2125
|
+
)
|
|
1948
2126
|
);
|
|
1949
|
-
for (const row of
|
|
2127
|
+
for (const row of pages.flat()) {
|
|
1950
2128
|
const key = row.tx_hash.toLowerCase();
|
|
1951
2129
|
const list = result.get(key);
|
|
1952
2130
|
if (list) list.push(row);
|
|
@@ -1974,19 +2152,52 @@ var IndexerQueries = class {
|
|
|
1974
2152
|
// ---- value movement ------------------------------------------------------
|
|
1975
2153
|
async deposits(vault2, options = {}) {
|
|
1976
2154
|
const { limit, offset } = bounds(options);
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2155
|
+
return this.client.selectPage(
|
|
2156
|
+
"deposits",
|
|
2157
|
+
DepositSchema,
|
|
2158
|
+
limit,
|
|
2159
|
+
(q) => q.select("*", { count: "estimated" }).eq("wallet_address", lower(vault2)).order("deposited_at_block", { ascending: false }).range(offset, offset + limit)
|
|
2160
|
+
);
|
|
1982
2161
|
}
|
|
1983
2162
|
async tokenTransfers(vault2, options = {}) {
|
|
1984
2163
|
const { limit, offset } = bounds(options);
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2164
|
+
return this.client.selectPage(
|
|
2165
|
+
"token_transfers",
|
|
2166
|
+
TokenTransferSchema,
|
|
2167
|
+
limit,
|
|
2168
|
+
(q) => q.select("*", { count: "estimated" }).eq("wallet_address", lower(vault2)).order("block_number", { ascending: false }).range(offset, offset + limit)
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Scan up to `budget` of the most recent transfer rows, paging past {@link MAX_LIMIT}.
|
|
2173
|
+
*
|
|
2174
|
+
* `tokenTransfers` clamps a single request, so a caller asking for more than the cap
|
|
2175
|
+
* silently received a short page — and then computed "did I see everything?" from
|
|
2176
|
+
* that short page's `hasMore`, which reported truncation the caller had not actually
|
|
2177
|
+
* hit. Paging here keeps the caller's budget meaningful and makes the returned
|
|
2178
|
+
* `hasMore` mean what it says: rows exist beyond the budget that was scanned.
|
|
2179
|
+
*
|
|
2180
|
+
* Pages by offset over a descending order, so a transfer landing mid-scan can shift
|
|
2181
|
+
* rows by one. That is acceptable for token *discovery* — a duplicate collapses into
|
|
2182
|
+
* the same map key and a missed row costs at most one candidate that the next call
|
|
2183
|
+
* picks up. Do not reuse this for anything that must see each row exactly once.
|
|
2184
|
+
*/
|
|
2185
|
+
async tokenTransferScan(vault2, budget) {
|
|
2186
|
+
const target = Math.max(1, Math.floor(budget));
|
|
2187
|
+
const data = [];
|
|
2188
|
+
let total = 0;
|
|
2189
|
+
let hasMore = false;
|
|
2190
|
+
while (data.length < target) {
|
|
2191
|
+
const page = await this.tokenTransfers(vault2, {
|
|
2192
|
+
limit: Math.min(MAX_LIMIT, target - data.length),
|
|
2193
|
+
offset: data.length
|
|
2194
|
+
});
|
|
2195
|
+
data.push(...page.data);
|
|
2196
|
+
total = page.total;
|
|
2197
|
+
hasMore = page.hasMore;
|
|
2198
|
+
if (page.data.length === 0 || !page.hasMore) break;
|
|
2199
|
+
}
|
|
2200
|
+
return { data, total, hasMore };
|
|
1990
2201
|
}
|
|
1991
2202
|
async tokens(addresses) {
|
|
1992
2203
|
if (addresses.length === 0) return [];
|
|
@@ -2209,8 +2420,6 @@ function deriveRecoveryStatus(state, at = nowSeconds()) {
|
|
|
2209
2420
|
}
|
|
2210
2421
|
|
|
2211
2422
|
// src/recovery.ts
|
|
2212
|
-
var SENTINEL = "0x0000000000000000000000000000000000000001";
|
|
2213
|
-
var ZERO = "0x0000000000000000000000000000000000000000";
|
|
2214
2423
|
var RecoveryModule = class {
|
|
2215
2424
|
constructor(ctx) {
|
|
2216
2425
|
this.ctx = ctx;
|
|
@@ -2232,14 +2441,23 @@ var RecoveryModule = class {
|
|
|
2232
2441
|
get vault() {
|
|
2233
2442
|
return this.ctx.vaultAddress;
|
|
2234
2443
|
}
|
|
2444
|
+
/**
|
|
2445
|
+
* The vault this module acts on, through the same retrying facade every other vault
|
|
2446
|
+
* read in the SDK uses. Recovery reaches into the vault for three checks — is the
|
|
2447
|
+
* module enabled, is the caller an owner, who are the current owners — and each of
|
|
2448
|
+
* them gates a write, so none of them should be the one read that silently skips
|
|
2449
|
+
* transient-failure handling.
|
|
2450
|
+
*/
|
|
2451
|
+
vaultContract() {
|
|
2452
|
+
return new VaultContract(this.ctx.connection.vault(this.vault), this.ctx.connection.retry);
|
|
2453
|
+
}
|
|
2235
2454
|
// =========================================================================
|
|
2236
2455
|
// Reads
|
|
2237
2456
|
// =========================================================================
|
|
2238
2457
|
/** Whether the module is currently enabled on the vault. */
|
|
2239
2458
|
async isEnabled() {
|
|
2240
2459
|
if (!this.ctx.moduleAddress) return false;
|
|
2241
|
-
|
|
2242
|
-
return vault2.getFunction("isModuleEnabled(address)")(this.address);
|
|
2460
|
+
return this.vaultContract().isModuleEnabled(this.address);
|
|
2243
2461
|
}
|
|
2244
2462
|
async config() {
|
|
2245
2463
|
const raw = await this.contract().getRecoveryConfig(this.vault);
|
|
@@ -2297,7 +2515,7 @@ var RecoveryModule = class {
|
|
|
2297
2515
|
*/
|
|
2298
2516
|
async history(options = {}) {
|
|
2299
2517
|
const queries = this.ctx.queries;
|
|
2300
|
-
if (!queries)
|
|
2518
|
+
if (!queries) throw new NoIndexerError("Listing recovery history");
|
|
2301
2519
|
const rows = await queries.recoveryHistory(this.vault, options);
|
|
2302
2520
|
return rows.map((row) => ({
|
|
2303
2521
|
hash: row.recovery_hash,
|
|
@@ -2433,14 +2651,13 @@ var RecoveryModule = class {
|
|
|
2433
2651
|
{ retryableAt: request.executionTime }
|
|
2434
2652
|
);
|
|
2435
2653
|
}
|
|
2436
|
-
const
|
|
2437
|
-
const currentOwners = await vault2.getFunction("getOwners()")();
|
|
2654
|
+
const currentOwners = await this.vaultContract().getOwners();
|
|
2438
2655
|
const current = new Set(Array.from(currentOwners).map((o) => String(o).toLowerCase()));
|
|
2439
2656
|
const overlap = request.newOwners.filter((o) => current.has(o.toLowerCase())).length;
|
|
2440
2657
|
const peak = current.size + request.newOwners.length - overlap;
|
|
2441
|
-
if (peak >
|
|
2658
|
+
if (peak > MAX_OWNERS) {
|
|
2442
2659
|
throw new PreconditionError(
|
|
2443
|
-
`Executing this recovery would transiently hold ${peak} owners, above the
|
|
2660
|
+
`Executing this recovery would transiently hold ${peak} owners, above the ${MAX_OWNERS}-owner maximum \u2014 new owners are added before old ones are removed.`,
|
|
2444
2661
|
{
|
|
2445
2662
|
remediation: "Run a recovery that retains at least one existing owner, then a second recovery to replace it."
|
|
2446
2663
|
}
|
|
@@ -2455,8 +2672,7 @@ var RecoveryModule = class {
|
|
|
2455
2672
|
async cancel(recoveryHash) {
|
|
2456
2673
|
const hash = normalizeTxHash(recoveryHash, "recovery hash");
|
|
2457
2674
|
const caller = await this.ctx.connection.address();
|
|
2458
|
-
const
|
|
2459
|
-
const isOwner = await vault2.getFunction("isOwner(address)")(caller);
|
|
2675
|
+
const isOwner = await this.vaultContract().isOwner(caller);
|
|
2460
2676
|
if (!isOwner) {
|
|
2461
2677
|
throw new PreconditionError(
|
|
2462
2678
|
`Only a current owner of the vault can cancel a recovery. ${caller} is not one.`
|
|
@@ -2487,7 +2703,7 @@ var RecoveryModule = class {
|
|
|
2487
2703
|
this.isGuardian(address2),
|
|
2488
2704
|
this.contract().recoveryApprovals(this.vault, hash, address2),
|
|
2489
2705
|
this.isEnabled(),
|
|
2490
|
-
this.
|
|
2706
|
+
this.vaultContract().isOwner(address2)
|
|
2491
2707
|
]);
|
|
2492
2708
|
const now = nowSeconds();
|
|
2493
2709
|
const terminal = request.status === "executed" || request.status === "cancelled";
|
|
@@ -2612,7 +2828,7 @@ var RecoveryModule = class {
|
|
|
2612
2828
|
const seen = /* @__PURE__ */ new Set();
|
|
2613
2829
|
for (const owner of newOwners) {
|
|
2614
2830
|
const key = owner.toLowerCase();
|
|
2615
|
-
if (key ===
|
|
2831
|
+
if (key === ZERO_ADDRESS || key === SENTINEL_MODULES) {
|
|
2616
2832
|
throw new ValidationError(
|
|
2617
2833
|
`New owner ${owner} is reserved \u2014 the zero address and the module sentinel 0x\u202601 are rejected.`
|
|
2618
2834
|
);
|
|
@@ -2693,16 +2909,61 @@ function terminalReason(request) {
|
|
|
2693
2909
|
}
|
|
2694
2910
|
|
|
2695
2911
|
// src/balances.ts
|
|
2696
|
-
import {
|
|
2912
|
+
import { getAddress as getAddress4 } from "quais";
|
|
2913
|
+
|
|
2914
|
+
// src/chain/token-contract.ts
|
|
2915
|
+
var TOKEN_RETRY = { maxAttempts: 2, baseDelayMs: 150 };
|
|
2916
|
+
var TokenContract = class {
|
|
2917
|
+
constructor(contract, retry = {}) {
|
|
2918
|
+
this.contract = contract;
|
|
2919
|
+
this.retry = { ...TOKEN_RETRY, ...retry };
|
|
2920
|
+
}
|
|
2921
|
+
contract;
|
|
2922
|
+
retry;
|
|
2923
|
+
read(signature, ...args) {
|
|
2924
|
+
return withRetry(() => this.contract.getFunction(signature)(...args), this.retry);
|
|
2925
|
+
}
|
|
2926
|
+
/** ERC20 units held, or — for ERC721 — the number of tokens held. */
|
|
2927
|
+
balanceOf(owner) {
|
|
2928
|
+
return this.read("balanceOf(address)", owner);
|
|
2929
|
+
}
|
|
2930
|
+
/** ERC721 only. Reverts for a burned or never-minted id. */
|
|
2931
|
+
ownerOf(tokenId) {
|
|
2932
|
+
return this.read("ownerOf(uint256)", tokenId);
|
|
2933
|
+
}
|
|
2934
|
+
/** ERC1155 only. One call for many ids, so it stays a single round trip. */
|
|
2935
|
+
balanceOfBatch(owners, ids) {
|
|
2936
|
+
return this.read("balanceOfBatch(address[],uint256[])", owners, ids);
|
|
2937
|
+
}
|
|
2938
|
+
};
|
|
2939
|
+
|
|
2940
|
+
// src/pool.ts
|
|
2941
|
+
async function mapPooled(items, limit, fn) {
|
|
2942
|
+
const results = new Array(items.length);
|
|
2943
|
+
let next = 0;
|
|
2944
|
+
const worker = async () => {
|
|
2945
|
+
for (; ; ) {
|
|
2946
|
+
const i = next++;
|
|
2947
|
+
if (i >= items.length) return;
|
|
2948
|
+
results[i] = await fn(items[i], i);
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
2952
|
+
return results;
|
|
2953
|
+
}
|
|
2954
|
+
var DEFAULT_CONCURRENCY = 8;
|
|
2955
|
+
|
|
2956
|
+
// src/balances.ts
|
|
2697
2957
|
async function loadBalances(connection, queries, vault2, options = {}) {
|
|
2698
2958
|
const verify = options.verify !== false;
|
|
2699
2959
|
const maxTokens = options.maxTokens ?? 50;
|
|
2700
2960
|
const transferScanLimit = options.transferScanLimit ?? 500;
|
|
2701
2961
|
const maxTokenIdChecks = options.maxTokenIdChecks ?? 100;
|
|
2962
|
+
const concurrency = Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY);
|
|
2702
2963
|
const address2 = getAddress4(vault2);
|
|
2703
2964
|
const native = BigInt(await connection.provider.getBalance(address2));
|
|
2704
2965
|
if (!queries) return { native, tokens: [] };
|
|
2705
|
-
const transfers = await queries.
|
|
2966
|
+
const transfers = await queries.tokenTransferScan(address2, transferScanLimit);
|
|
2706
2967
|
const seen = /* @__PURE__ */ new Map();
|
|
2707
2968
|
for (const row of transfers.data) {
|
|
2708
2969
|
const key = row.token_address.toLowerCase();
|
|
@@ -2721,17 +2982,18 @@ async function loadBalances(connection, queries, vault2, options = {}) {
|
|
|
2721
2982
|
}
|
|
2722
2983
|
const metadata = await queries.tokens(candidates);
|
|
2723
2984
|
const byAddress = new Map(metadata.map((t) => [t.address.toLowerCase(), t]));
|
|
2724
|
-
const balances = await
|
|
2725
|
-
candidates
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2985
|
+
const balances = await mapPooled(
|
|
2986
|
+
candidates,
|
|
2987
|
+
concurrency,
|
|
2988
|
+
(token2) => buildBalance(
|
|
2989
|
+
connection,
|
|
2990
|
+
address2,
|
|
2991
|
+
token2,
|
|
2992
|
+
byAddress.get(token2),
|
|
2993
|
+
seen.get(token2) ?? [],
|
|
2994
|
+
verify,
|
|
2995
|
+
maxTokenIdChecks,
|
|
2996
|
+
concurrency
|
|
2735
2997
|
)
|
|
2736
2998
|
);
|
|
2737
2999
|
return {
|
|
@@ -2741,30 +3003,35 @@ async function loadBalances(connection, queries, vault2, options = {}) {
|
|
|
2741
3003
|
...Object.keys(truncated).length ? { truncated } : {}
|
|
2742
3004
|
};
|
|
2743
3005
|
}
|
|
2744
|
-
async function buildBalance(connection, vault2, token2, meta, transfers, verify, maxTokenIdChecks) {
|
|
3006
|
+
async function buildBalance(connection, vault2, token2, meta, transfers, verify, maxTokenIdChecks, concurrency) {
|
|
2745
3007
|
const standard = meta?.standard ?? inferStandard(transfers);
|
|
2746
3008
|
const replayed = replayBalance(transfers, standard);
|
|
2747
3009
|
const base = {
|
|
2748
3010
|
token: getAddress4(token2),
|
|
2749
3011
|
standard,
|
|
2750
|
-
|
|
2751
|
-
|
|
3012
|
+
// Deployer-controlled strings headed for someone's terminal. See `src/text.ts`.
|
|
3013
|
+
symbol: sanitizeText(meta?.symbol, 32) || "???",
|
|
3014
|
+
name: sanitizeText(meta?.name, 64) || "Unknown token",
|
|
2752
3015
|
decimals: meta?.decimals ?? (standard === "ERC20" ? 18 : 0),
|
|
2753
3016
|
balance: replayed.balance,
|
|
2754
3017
|
...replayed.tokenIds.length > 0 ? { tokenIds: replayed.tokenIds } : {},
|
|
2755
3018
|
verified: false
|
|
2756
3019
|
};
|
|
2757
3020
|
if (!verify) return base;
|
|
3021
|
+
const contract = new TokenContract(connection.token(token2, standard), connection.retry);
|
|
2758
3022
|
try {
|
|
2759
3023
|
if (standard === "ERC20") {
|
|
2760
|
-
|
|
2761
|
-
const balance = await contract2.getFunction("balanceOf(address)")(vault2);
|
|
2762
|
-
return { ...base, balance: BigInt(balance), verified: true };
|
|
3024
|
+
return { ...base, balance: BigInt(await contract.balanceOf(vault2)), verified: true };
|
|
2763
3025
|
}
|
|
2764
3026
|
if (standard === "ERC721") {
|
|
2765
|
-
const
|
|
2766
|
-
const
|
|
2767
|
-
|
|
3027
|
+
const count = await contract.balanceOf(vault2);
|
|
3028
|
+
const owned = await filterOwned(
|
|
3029
|
+
contract,
|
|
3030
|
+
vault2,
|
|
3031
|
+
replayed.tokenIds,
|
|
3032
|
+
maxTokenIdChecks,
|
|
3033
|
+
concurrency
|
|
3034
|
+
);
|
|
2768
3035
|
return {
|
|
2769
3036
|
...base,
|
|
2770
3037
|
balance: BigInt(count),
|
|
@@ -2773,10 +3040,9 @@ async function buildBalance(connection, vault2, token2, meta, transfers, verify,
|
|
|
2773
3040
|
verified: true
|
|
2774
3041
|
};
|
|
2775
3042
|
}
|
|
2776
|
-
const contract = new Contract2(token2, Erc1155Abi, connection.provider);
|
|
2777
3043
|
const ids = replayed.tokenIds;
|
|
2778
3044
|
if (ids.length === 0) return { ...base, verified: true };
|
|
2779
|
-
const amounts = await contract.
|
|
3045
|
+
const amounts = await contract.balanceOfBatch(
|
|
2780
3046
|
ids.map(() => vault2),
|
|
2781
3047
|
ids
|
|
2782
3048
|
);
|
|
@@ -2821,18 +3087,16 @@ function replayBalance(transfers, standard) {
|
|
|
2821
3087
|
}
|
|
2822
3088
|
return { balance, tokenIds };
|
|
2823
3089
|
}
|
|
2824
|
-
async function filterOwned(contract, vault2, ids, limit) {
|
|
3090
|
+
async function filterOwned(contract, vault2, ids, limit, concurrency) {
|
|
2825
3091
|
if (ids.length === 0) return [];
|
|
2826
|
-
const results = await
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
})
|
|
2835
|
-
);
|
|
3092
|
+
const results = await mapPooled(ids.slice(0, limit), concurrency, async (id) => {
|
|
3093
|
+
try {
|
|
3094
|
+
const owner = await contract.ownerOf(id);
|
|
3095
|
+
return owner.toLowerCase() === vault2.toLowerCase() ? id : null;
|
|
3096
|
+
} catch {
|
|
3097
|
+
return null;
|
|
3098
|
+
}
|
|
3099
|
+
});
|
|
2836
3100
|
return results.filter((id) => id !== null);
|
|
2837
3101
|
}
|
|
2838
3102
|
function inferStandard(transfers) {
|
|
@@ -2868,7 +3132,7 @@ function watchVault(client, vault2, handler, options = {}) {
|
|
|
2868
3132
|
const topics = options.topics ?? DEFAULT_TOPICS;
|
|
2869
3133
|
const address2 = vault2.toLowerCase();
|
|
2870
3134
|
const schema = client.config.schema;
|
|
2871
|
-
const channel = client.
|
|
3135
|
+
const channel = client.channel(`quaivault:${schema}:${address2}`);
|
|
2872
3136
|
for (const topic of topics) {
|
|
2873
3137
|
const table = TABLE_FOR[topic];
|
|
2874
3138
|
channel.on(
|
|
@@ -2898,7 +3162,7 @@ function watchVault(client, vault2, handler, options = {}) {
|
|
|
2898
3162
|
return {
|
|
2899
3163
|
topics,
|
|
2900
3164
|
async unsubscribe() {
|
|
2901
|
-
await client.
|
|
3165
|
+
await client.removeChannel(channel);
|
|
2902
3166
|
}
|
|
2903
3167
|
};
|
|
2904
3168
|
}
|
|
@@ -3378,7 +3642,7 @@ function extractProposedTxHash(receipt, vault2) {
|
|
|
3378
3642
|
}
|
|
3379
3643
|
|
|
3380
3644
|
// src/vault.ts
|
|
3381
|
-
var Vault = class {
|
|
3645
|
+
var Vault = class _Vault {
|
|
3382
3646
|
address;
|
|
3383
3647
|
/** Guardian-based recovery for this vault. */
|
|
3384
3648
|
recovery;
|
|
@@ -3421,6 +3685,21 @@ var Vault = class {
|
|
|
3421
3685
|
if (!this.ctx.queries) throw new NoIndexerError(operation);
|
|
3422
3686
|
return this.ctx.queries;
|
|
3423
3687
|
}
|
|
3688
|
+
/**
|
|
3689
|
+
* The indexer's head, for stamping onto records read from it.
|
|
3690
|
+
*
|
|
3691
|
+
* Reads the cached health result rather than querying `indexer_state` directly.
|
|
3692
|
+
* `useIndexer()` has just called `health()` on this same code path and the result is
|
|
3693
|
+
* cached, so this is free — whereas a `state()` call is a second round trip per
|
|
3694
|
+
* hydration, paid on every indexed read purely to fill one advisory field.
|
|
3695
|
+
*
|
|
3696
|
+
* Undefined when the indexer is not answering: a head of 0 would read as "indexed at
|
|
3697
|
+
* genesis" rather than "unknown".
|
|
3698
|
+
*/
|
|
3699
|
+
async indexerHead() {
|
|
3700
|
+
const health = await this.ctx.indexer?.health();
|
|
3701
|
+
return health?.available ? health.lastIndexedBlock : void 0;
|
|
3702
|
+
}
|
|
3424
3703
|
contract(write = false) {
|
|
3425
3704
|
return new VaultContract(this.ctx.connection.vault(this.address, write), this.ctx.connection.retry);
|
|
3426
3705
|
}
|
|
@@ -3448,8 +3727,48 @@ var Vault = class {
|
|
|
3448
3727
|
balance: BigInt(balance)
|
|
3449
3728
|
};
|
|
3450
3729
|
}
|
|
3451
|
-
/**
|
|
3730
|
+
/**
|
|
3731
|
+
* Capture owners and threshold once, for reuse across a burst of reads.
|
|
3732
|
+
*
|
|
3733
|
+
* Pair with {@link pinned}:
|
|
3734
|
+
*
|
|
3735
|
+
* ```ts
|
|
3736
|
+
* const view = await vault.view();
|
|
3737
|
+
* const pinned = vault.pinned(view);
|
|
3738
|
+
* // every read below shares one owner/threshold read
|
|
3739
|
+
* const txs = await pinned.transactions(hashes);
|
|
3740
|
+
* ```
|
|
3741
|
+
*/
|
|
3742
|
+
async view() {
|
|
3743
|
+
const fromIndexer = await this.useIndexer();
|
|
3744
|
+
const [owners, threshold, indexedAtBlock] = await Promise.all([
|
|
3745
|
+
this.owners(),
|
|
3746
|
+
this.threshold(),
|
|
3747
|
+
this.indexerHead()
|
|
3748
|
+
]);
|
|
3749
|
+
return {
|
|
3750
|
+
owners,
|
|
3751
|
+
threshold,
|
|
3752
|
+
...indexedAtBlock !== void 0 ? { indexedAtBlock } : {},
|
|
3753
|
+
capturedAt: nowSeconds(),
|
|
3754
|
+
source: fromIndexer ? "indexer" : "chain"
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
/**
|
|
3758
|
+
* A handle that answers {@link owners} and {@link threshold} from `view` instead of
|
|
3759
|
+
* re-reading them.
|
|
3760
|
+
*
|
|
3761
|
+
* Reads only. Write preconditions go through `chainOwners` / `chainThreshold`, which
|
|
3762
|
+
* bypass the snapshot by construction — pinning a view can never cause this SDK to
|
|
3763
|
+
* sign against a stale owner set. Everything else about the handle, including the
|
|
3764
|
+
* connection and the recovery module, is shared with the original.
|
|
3765
|
+
*/
|
|
3766
|
+
pinned(view) {
|
|
3767
|
+
return new _Vault(this.address, { ...this.ctx, view });
|
|
3768
|
+
}
|
|
3769
|
+
/** Active owners. Prefers a pinned view, then the indexer, then chain. */
|
|
3452
3770
|
async owners() {
|
|
3771
|
+
if (this.ctx.view) return this.ctx.view.owners;
|
|
3453
3772
|
if (await this.useIndexer()) {
|
|
3454
3773
|
try {
|
|
3455
3774
|
const owners = await this.requireQueries("owners").owners(this.address);
|
|
@@ -3457,12 +3776,40 @@ var Vault = class {
|
|
|
3457
3776
|
} catch {
|
|
3458
3777
|
}
|
|
3459
3778
|
}
|
|
3779
|
+
return this.chainOwners();
|
|
3780
|
+
}
|
|
3781
|
+
/**
|
|
3782
|
+
* Owners straight from the chain, bypassing the indexer entirely.
|
|
3783
|
+
*
|
|
3784
|
+
* {@link owners} prefers the indexer, which is right for display and wrong for
|
|
3785
|
+
* anything that gates a write. A lagging indexer would let the SDK build a proposal
|
|
3786
|
+
* against an owner set that no longer exists — admitting a `removeOwner` that drops
|
|
3787
|
+
* the vault below its threshold, or rejecting one that is perfectly valid. Every
|
|
3788
|
+
* propose-time precondition reads through here instead.
|
|
3789
|
+
*/
|
|
3790
|
+
async chainOwners() {
|
|
3460
3791
|
return (await this.contract().getOwners()).map((o) => getAddress5(String(o)));
|
|
3461
3792
|
}
|
|
3462
3793
|
async isOwner(address2) {
|
|
3463
3794
|
return this.contract().isOwner(getAddress5(address2));
|
|
3464
3795
|
}
|
|
3796
|
+
/** Approvals required to execute. Prefers a pinned view, then the indexer, then chain. */
|
|
3465
3797
|
async threshold() {
|
|
3798
|
+
if (this.ctx.view) return this.ctx.view.threshold;
|
|
3799
|
+
if (await this.useIndexer()) {
|
|
3800
|
+
try {
|
|
3801
|
+
const wallet = await this.requireQueries("threshold").wallet(this.address);
|
|
3802
|
+
if (wallet) return wallet.threshold;
|
|
3803
|
+
} catch {
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
return this.chainThreshold();
|
|
3807
|
+
}
|
|
3808
|
+
/**
|
|
3809
|
+
* Threshold straight from the chain. The write-path counterpart to {@link threshold},
|
|
3810
|
+
* for the same reason {@link chainOwners} exists.
|
|
3811
|
+
*/
|
|
3812
|
+
async chainThreshold() {
|
|
3466
3813
|
return Number(await this.contract().threshold());
|
|
3467
3814
|
}
|
|
3468
3815
|
async balance() {
|
|
@@ -3515,6 +3862,53 @@ var Vault = class {
|
|
|
3515
3862
|
}
|
|
3516
3863
|
return this.fromChain(hash);
|
|
3517
3864
|
}
|
|
3865
|
+
/**
|
|
3866
|
+
* Several transactions at once, keyed by hash.
|
|
3867
|
+
*
|
|
3868
|
+
* The plural form exists because the singular one is expensive to loop: each
|
|
3869
|
+
* `transaction()` re-reads the owner set, the threshold and the indexer head, so
|
|
3870
|
+
* fetching fifty costs fifty times that. This resolves the whole set the way the
|
|
3871
|
+
* listing methods already do — one query for the rows, one owner/threshold read
|
|
3872
|
+
* shared across them, one batched confirmations query.
|
|
3873
|
+
*
|
|
3874
|
+
* Hashes the indexer does not have fall back to chain reads, bounded by a pool.
|
|
3875
|
+
* Hashes that exist nowhere are simply absent from the map rather than throwing:
|
|
3876
|
+
* one unknown hash should not lose the caller the other forty-nine.
|
|
3877
|
+
*/
|
|
3878
|
+
async transactions(txHashes) {
|
|
3879
|
+
const hashes = [...new Set(txHashes.map((h) => normalizeTxHash(h)))];
|
|
3880
|
+
const found = /* @__PURE__ */ new Map();
|
|
3881
|
+
if (hashes.length === 0) return found;
|
|
3882
|
+
if (await this.useIndexer()) {
|
|
3883
|
+
try {
|
|
3884
|
+
const queries = this.requireQueries("Reading transactions");
|
|
3885
|
+
const [rows, owners, threshold] = await Promise.all([
|
|
3886
|
+
queries.transactionsByHash(this.address, hashes),
|
|
3887
|
+
this.owners(),
|
|
3888
|
+
this.threshold()
|
|
3889
|
+
]);
|
|
3890
|
+
for (const tx of await this.hydrateRows(rows, owners, threshold)) {
|
|
3891
|
+
found.set(tx.hash.toLowerCase(), tx);
|
|
3892
|
+
}
|
|
3893
|
+
} catch {
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
const missing = hashes.filter((hash) => !found.has(hash));
|
|
3897
|
+
if (missing.length > 0) {
|
|
3898
|
+
const resolved = await mapPooled(missing, DEFAULT_CONCURRENCY, async (hash) => {
|
|
3899
|
+
try {
|
|
3900
|
+
return await this.fromChain(hash);
|
|
3901
|
+
} catch (err) {
|
|
3902
|
+
if (err instanceof NotFoundError) return null;
|
|
3903
|
+
throw err;
|
|
3904
|
+
}
|
|
3905
|
+
});
|
|
3906
|
+
for (const tx of resolved) {
|
|
3907
|
+
if (tx) found.set(tx.hash.toLowerCase(), tx);
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
return found;
|
|
3911
|
+
}
|
|
3518
3912
|
async pendingTransactions(options = {}) {
|
|
3519
3913
|
const queries = this.requireQueries("Listing pending transactions");
|
|
3520
3914
|
const [rows, owners, threshold] = await Promise.all([
|
|
@@ -3545,7 +3939,7 @@ var Vault = class {
|
|
|
3545
3939
|
this.address,
|
|
3546
3940
|
rows.map((r) => r.tx_hash)
|
|
3547
3941
|
);
|
|
3548
|
-
const indexedAtBlock =
|
|
3942
|
+
const indexedAtBlock = await this.indexerHead();
|
|
3549
3943
|
return rows.map((row) => {
|
|
3550
3944
|
const confirmed = (confirmations.get(row.tx_hash.toLowerCase()) ?? []).map(
|
|
3551
3945
|
(c) => c.owner_address
|
|
@@ -3568,7 +3962,7 @@ var Vault = class {
|
|
|
3568
3962
|
]);
|
|
3569
3963
|
if (!row) return null;
|
|
3570
3964
|
const confirmations = await queries.confirmations(this.address, hash);
|
|
3571
|
-
const indexedAtBlock =
|
|
3965
|
+
const indexedAtBlock = await this.indexerHead();
|
|
3572
3966
|
return this.buildTransaction({
|
|
3573
3967
|
row,
|
|
3574
3968
|
confirmedBy: confirmations.filter((c) => c.is_active).map((c) => c.owner_address),
|
|
@@ -3631,7 +4025,10 @@ var Vault = class {
|
|
|
3631
4025
|
value,
|
|
3632
4026
|
data,
|
|
3633
4027
|
proposer: getAddress5(row.submitted_by),
|
|
3634
|
-
|
|
4028
|
+
// The indexer records the block, not the chain timestamp — they are not
|
|
4029
|
+
// interchangeable, and reporting a block number as `proposedAt` renders as 1970.
|
|
4030
|
+
proposedAt: 0,
|
|
4031
|
+
proposedAtBlock: toNumber(row.submitted_at_block),
|
|
3635
4032
|
kind: decodeResult.kind,
|
|
3636
4033
|
decoded: decodeResult.decoded,
|
|
3637
4034
|
summary: decodeResult.summary,
|
|
@@ -3722,6 +4119,18 @@ var Vault = class {
|
|
|
3722
4119
|
*/
|
|
3723
4120
|
async affordances(txHash, caller) {
|
|
3724
4121
|
const hash = normalizeTxHash(txHash);
|
|
4122
|
+
return this.resolveAffordances(hash, this.transaction(hash), caller);
|
|
4123
|
+
}
|
|
4124
|
+
/**
|
|
4125
|
+
* Shared body of {@link affordances}, taking the transaction either as a value or as
|
|
4126
|
+
* an in-flight promise.
|
|
4127
|
+
*
|
|
4128
|
+
* The promise form is the point: a caller that already holds the record (like
|
|
4129
|
+
* {@link describe}) passes it and skips a redundant fetch, while `affordances()`
|
|
4130
|
+
* passes the unawaited promise so the transaction read still overlaps the two
|
|
4131
|
+
* ownership probes rather than serialising behind them.
|
|
4132
|
+
*/
|
|
4133
|
+
async resolveAffordances(hash, transaction, caller) {
|
|
3725
4134
|
const who = caller ?? await this.ctx.connection.addressOrNull();
|
|
3726
4135
|
if (!who) {
|
|
3727
4136
|
throw new ValidationError(
|
|
@@ -3730,7 +4139,7 @@ var Vault = class {
|
|
|
3730
4139
|
}
|
|
3731
4140
|
const vault2 = this.contract();
|
|
3732
4141
|
const [tx, isOwner, hasApproved] = await Promise.all([
|
|
3733
|
-
|
|
4142
|
+
transaction,
|
|
3734
4143
|
vault2.isOwner(getAddress5(who)),
|
|
3735
4144
|
vault2.hasApproved(hash, getAddress5(who))
|
|
3736
4145
|
]);
|
|
@@ -3825,7 +4234,7 @@ var Vault = class {
|
|
|
3825
4234
|
// --- self-calls -------------------------------------------------------
|
|
3826
4235
|
addOwner: async (owner, options = {}) => this.proposeSelfCall(selfCall.addOwner(assertQuaiAddress(owner, "owner")), options),
|
|
3827
4236
|
removeOwner: async (owner, options = {}) => {
|
|
3828
|
-
const [owners, threshold] = await Promise.all([this.
|
|
4237
|
+
const [owners, threshold] = await Promise.all([this.chainOwners(), this.chainThreshold()]);
|
|
3829
4238
|
if (!owners.some((o) => o.toLowerCase() === owner.toLowerCase())) {
|
|
3830
4239
|
throw new PreconditionError(`${owner} is not an owner of this vault.`);
|
|
3831
4240
|
}
|
|
@@ -3838,7 +4247,7 @@ var Vault = class {
|
|
|
3838
4247
|
return this.proposeSelfCall(selfCall.removeOwner(getAddress5(owner)), options);
|
|
3839
4248
|
},
|
|
3840
4249
|
changeThreshold: async (threshold, options = {}) => {
|
|
3841
|
-
const owners = await this.
|
|
4250
|
+
const owners = await this.chainOwners();
|
|
3842
4251
|
if (threshold < 1 || threshold > owners.length) {
|
|
3843
4252
|
throw new ValidationError(
|
|
3844
4253
|
`Invalid threshold ${threshold}: this vault has ${owners.length} owners.`
|
|
@@ -4272,9 +4681,7 @@ var Vault = class {
|
|
|
4272
4681
|
const pollIntervalMs = options.pollIntervalMs ?? 15e3;
|
|
4273
4682
|
const deadline = Date.now() + timeoutMs;
|
|
4274
4683
|
for (; ; ) {
|
|
4275
|
-
if (options.signal?.aborted)
|
|
4276
|
-
throw new PreconditionError("Waiting for the transaction was aborted.");
|
|
4277
|
-
}
|
|
4684
|
+
if (options.signal?.aborted) throw new AbortError("Waiting for the transaction");
|
|
4278
4685
|
const tx = await this.fromChain(hash);
|
|
4279
4686
|
options.onPoll?.(tx);
|
|
4280
4687
|
if (tx.status === "ready") return tx;
|
|
@@ -4331,7 +4738,7 @@ var Vault = class {
|
|
|
4331
4738
|
if (tx.decodedRevert) lines.push(` revert: ${tx.decodedRevert.message}`);
|
|
4332
4739
|
lines.push(` source: ${tx.source}`);
|
|
4333
4740
|
if (caller || this.ctx.connection.hasSigner()) {
|
|
4334
|
-
const affordances = await this.
|
|
4741
|
+
const affordances = await this.resolveAffordances(tx.hash, tx, caller);
|
|
4335
4742
|
const allowed = affordances.filter((a) => a.allowed).map((a) => a.action);
|
|
4336
4743
|
lines.push(` you can: ${allowed.length > 0 ? allowed.join(", ") : "nothing right now"}`);
|
|
4337
4744
|
}
|
|
@@ -4462,8 +4869,11 @@ function connect(options = {}) {
|
|
|
4462
4869
|
}
|
|
4463
4870
|
var QuaiVault = { connect };
|
|
4464
4871
|
export {
|
|
4872
|
+
AbortError,
|
|
4465
4873
|
ConfigError,
|
|
4466
4874
|
Connection,
|
|
4875
|
+
DEFAULT_CONCURRENCY,
|
|
4876
|
+
DEFAULT_TEXT_LIMIT,
|
|
4467
4877
|
ENV_VARS,
|
|
4468
4878
|
Erc1155Abi,
|
|
4469
4879
|
Erc20Abi,
|
|
@@ -4496,9 +4906,11 @@ export {
|
|
|
4496
4906
|
SaltMiningError,
|
|
4497
4907
|
SocialRecoveryModuleAbi,
|
|
4498
4908
|
StaleProposalError,
|
|
4909
|
+
TokenContract,
|
|
4499
4910
|
ValidationError,
|
|
4500
4911
|
Vault,
|
|
4501
4912
|
VaultContract,
|
|
4913
|
+
ZERO_ADDRESS,
|
|
4502
4914
|
allowedActions,
|
|
4503
4915
|
assertQuaiAddress,
|
|
4504
4916
|
assertQuaiAddresses,
|
|
@@ -4507,6 +4919,7 @@ export {
|
|
|
4507
4919
|
computeBytecodeHash,
|
|
4508
4920
|
computeFullSalt,
|
|
4509
4921
|
connect,
|
|
4922
|
+
createWorkerThreadsStrategy,
|
|
4510
4923
|
decodeCall,
|
|
4511
4924
|
decodeMultiSendPayload,
|
|
4512
4925
|
decodeRevert,
|
|
@@ -4529,6 +4942,7 @@ export {
|
|
|
4529
4942
|
knownErrorSelectors,
|
|
4530
4943
|
loadBalances,
|
|
4531
4944
|
mainnet,
|
|
4945
|
+
mapPooled,
|
|
4532
4946
|
mineSalt,
|
|
4533
4947
|
minimumExpiration,
|
|
4534
4948
|
networks,
|
|
@@ -4537,6 +4951,7 @@ export {
|
|
|
4537
4951
|
recoveryCall,
|
|
4538
4952
|
remediationFor,
|
|
4539
4953
|
resolveConfig,
|
|
4954
|
+
sanitizeText,
|
|
4540
4955
|
selfCall,
|
|
4541
4956
|
shardPrefixOf,
|
|
4542
4957
|
syncStrategy,
|