@arkade-os/sdk 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/wallet/delegator.js +44 -0
- package/dist/cjs/wallet/expo/background.js +4 -8
- package/dist/cjs/worker/expo/index.js +2 -1
- package/dist/cjs/worker/expo/taskRunner.js +25 -0
- package/dist/cjs/worker/messageBus.js +43 -7
- package/dist/esm/wallet/delegator.js +44 -0
- package/dist/esm/wallet/expo/background.js +6 -10
- package/dist/esm/worker/expo/index.js +1 -1
- package/dist/esm/worker/expo/taskRunner.js +25 -1
- package/dist/esm/worker/messageBus.js +43 -7
- package/dist/types/worker/expo/index.d.ts +2 -2
- package/dist/types/worker/expo/taskRunner.d.ts +21 -0
- package/dist/types/worker/messageBus.d.ts +11 -1
- package/package.json +1 -1
|
@@ -7,6 +7,8 @@ const base_2 = require("../script/base");
|
|
|
7
7
|
const forfeit_1 = require("../forfeit");
|
|
8
8
|
const btc_signer_1 = require("@scure/btc-signer");
|
|
9
9
|
const networks_1 = require("../networks");
|
|
10
|
+
const asset_1 = require("./asset");
|
|
11
|
+
const extension_1 = require("../extension");
|
|
10
12
|
class DelegatorManagerImpl {
|
|
11
13
|
constructor(delegatorProvider, arkInfoProvider, identity) {
|
|
12
14
|
this.delegatorProvider = delegatorProvider;
|
|
@@ -217,6 +219,48 @@ async function makeDelegateForfeitTx(input, connectorAmount, delegatePubkey, for
|
|
|
217
219
|
return identity.sign(tx);
|
|
218
220
|
}
|
|
219
221
|
async function makeSignedDelegateIntent(identity, coins, outputs, onchainOutputsIndexes, cosignerPubKeys, validAt) {
|
|
222
|
+
// if some of the inputs hold assets, build the asset packet and append as output
|
|
223
|
+
// in the intent proof tx, there is a "fake" input at index 0
|
|
224
|
+
// so the real coin indices are offset by +1
|
|
225
|
+
const assetInputs = new Map();
|
|
226
|
+
for (let i = 0; i < coins.length; i++) {
|
|
227
|
+
if ("assets" in coins[i]) {
|
|
228
|
+
const assets = coins[i].assets;
|
|
229
|
+
if (assets && assets.length > 0) {
|
|
230
|
+
assetInputs.set(i + 1, assets);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
let outputAssets;
|
|
235
|
+
let assetOutputIndex; // where to send the assets
|
|
236
|
+
if (assetInputs.size > 0) {
|
|
237
|
+
// collect all input assets and assign them to the first offchain output
|
|
238
|
+
const allAssets = new Map();
|
|
239
|
+
for (const [, assets] of assetInputs) {
|
|
240
|
+
for (const asset of assets) {
|
|
241
|
+
const existing = allAssets.get(asset.assetId) ?? 0n;
|
|
242
|
+
allAssets.set(asset.assetId, existing + BigInt(asset.amount));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
outputAssets = [];
|
|
246
|
+
for (const [assetId, amount] of allAssets) {
|
|
247
|
+
outputAssets.push({ assetId, amount: Number(amount) });
|
|
248
|
+
}
|
|
249
|
+
const firstOffchainIndex = outputs.findIndex((_, i) => !onchainOutputsIndexes.includes(i));
|
|
250
|
+
if (firstOffchainIndex === -1) {
|
|
251
|
+
throw new Error("Cannot settle assets without an offchain output");
|
|
252
|
+
}
|
|
253
|
+
assetOutputIndex = firstOffchainIndex;
|
|
254
|
+
}
|
|
255
|
+
const recipients = outputs.map((output, i) => ({
|
|
256
|
+
address: "", // not needed for asset packet creation
|
|
257
|
+
amount: Number(output.amount),
|
|
258
|
+
assets: i === assetOutputIndex ? outputAssets : undefined,
|
|
259
|
+
}));
|
|
260
|
+
if (outputAssets && outputAssets.length > 0) {
|
|
261
|
+
const assetPacket = (0, asset_1.createAssetPacket)(assetInputs, recipients);
|
|
262
|
+
outputs.push(extension_1.Extension.create([assetPacket]).txOut());
|
|
263
|
+
}
|
|
220
264
|
const message = {
|
|
221
265
|
type: "register",
|
|
222
266
|
onchain_output_indexes: onchainOutputsIndexes,
|
|
@@ -66,7 +66,7 @@ function defineExpoBackgroundTask(taskName, options) {
|
|
|
66
66
|
const arkProvider = new expoArk_1.ExpoArkProvider(config.arkServerUrl);
|
|
67
67
|
// Reconstruct default offchainTapscript as fallback
|
|
68
68
|
// for VTXOs not associated with a contract.
|
|
69
|
-
const
|
|
69
|
+
const offchainTapscript = new default_1.DefaultVtxo.Script({
|
|
70
70
|
pubKey: base_1.hex.decode(config.pubkeyHex),
|
|
71
71
|
serverPubKey: base_1.hex.decode(config.serverPubKeyHex),
|
|
72
72
|
csvTimelock: {
|
|
@@ -74,18 +74,14 @@ function defineExpoBackgroundTask(taskName, options) {
|
|
|
74
74
|
type: config.exitTimelockType,
|
|
75
75
|
},
|
|
76
76
|
});
|
|
77
|
-
|
|
77
|
+
const deps = (0, taskRunner_1.createTaskDependencies)({
|
|
78
78
|
walletRepository,
|
|
79
79
|
contractRepository,
|
|
80
80
|
indexerProvider,
|
|
81
81
|
arkProvider,
|
|
82
|
-
|
|
83
|
-
if (contract) {
|
|
84
|
-
return (0, utils_1.extendVtxoFromContract)(vtxo, contract);
|
|
85
|
-
}
|
|
86
|
-
return (0, utils_1.extendVirtualCoin)({ offchainTapscript: defaultTapscript }, vtxo);
|
|
87
|
-
},
|
|
82
|
+
offchainTapscript,
|
|
88
83
|
});
|
|
84
|
+
await (0, taskRunner_1.runTasks)(taskQueue, processors, deps);
|
|
89
85
|
// Acknowledge outbox results (no foreground to consume them)
|
|
90
86
|
const results = await taskQueue.getResults();
|
|
91
87
|
if (results.length > 0) {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CONTRACT_POLL_TASK_TYPE = exports.contractPollProcessor = exports.runTasks = exports.AsyncStorageTaskQueue = exports.InMemoryTaskQueue = void 0;
|
|
3
|
+
exports.CONTRACT_POLL_TASK_TYPE = exports.contractPollProcessor = exports.createTaskDependencies = exports.runTasks = exports.AsyncStorageTaskQueue = exports.InMemoryTaskQueue = void 0;
|
|
4
4
|
var taskQueue_1 = require("./taskQueue");
|
|
5
5
|
Object.defineProperty(exports, "InMemoryTaskQueue", { enumerable: true, get: function () { return taskQueue_1.InMemoryTaskQueue; } });
|
|
6
6
|
var asyncStorageTaskQueue_1 = require("./asyncStorageTaskQueue");
|
|
7
7
|
Object.defineProperty(exports, "AsyncStorageTaskQueue", { enumerable: true, get: function () { return asyncStorageTaskQueue_1.AsyncStorageTaskQueue; } });
|
|
8
8
|
var taskRunner_1 = require("./taskRunner");
|
|
9
9
|
Object.defineProperty(exports, "runTasks", { enumerable: true, get: function () { return taskRunner_1.runTasks; } });
|
|
10
|
+
Object.defineProperty(exports, "createTaskDependencies", { enumerable: true, get: function () { return taskRunner_1.createTaskDependencies; } });
|
|
10
11
|
var processors_1 = require("./processors");
|
|
11
12
|
Object.defineProperty(exports, "contractPollProcessor", { enumerable: true, get: function () { return processors_1.contractPollProcessor; } });
|
|
12
13
|
Object.defineProperty(exports, "CONTRACT_POLL_TASK_TYPE", { enumerable: true, get: function () { return processors_1.CONTRACT_POLL_TASK_TYPE; } });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runTasks = runTasks;
|
|
4
|
+
exports.createTaskDependencies = createTaskDependencies;
|
|
4
5
|
const utils_1 = require("../../wallet/utils");
|
|
5
6
|
/**
|
|
6
7
|
* Run all pending tasks from the queue through matching processors.
|
|
@@ -55,3 +56,27 @@ async function runTasks(queue, processors, deps) {
|
|
|
55
56
|
}
|
|
56
57
|
return results;
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the {@link TaskDependencies} needed by task processors
|
|
61
|
+
* (e.g. {@link import("./processors").contractPollProcessor}).
|
|
62
|
+
*
|
|
63
|
+
* This is the same construction that `defineExpoBackgroundTask` does
|
|
64
|
+
* internally, extracted so that consumers with custom schedulers
|
|
65
|
+
* (e.g. bare React Native with `react-native-background-fetch`)
|
|
66
|
+
* can build deps without depending on Expo.
|
|
67
|
+
*/
|
|
68
|
+
function createTaskDependencies(options) {
|
|
69
|
+
const { walletRepository, contractRepository, indexerProvider, arkProvider, offchainTapscript, } = options;
|
|
70
|
+
return {
|
|
71
|
+
walletRepository,
|
|
72
|
+
contractRepository,
|
|
73
|
+
indexerProvider,
|
|
74
|
+
arkProvider,
|
|
75
|
+
extendVtxo: (vtxo, contract) => {
|
|
76
|
+
if (contract) {
|
|
77
|
+
return (0, utils_1.extendVtxoFromContract)(vtxo, contract);
|
|
78
|
+
}
|
|
79
|
+
return (0, utils_1.extendVirtualCoin)({ offchainTapscript }, vtxo);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -9,7 +9,7 @@ const identity_1 = require("../identity");
|
|
|
9
9
|
const wallet_1 = require("../wallet/wallet");
|
|
10
10
|
const base_1 = require("@scure/base");
|
|
11
11
|
class MessageBus {
|
|
12
|
-
constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, debug = false, buildServices, }) {
|
|
12
|
+
constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, messageTimeoutMs = 30000, debug = false, buildServices, }) {
|
|
13
13
|
this.walletRepository = walletRepository;
|
|
14
14
|
this.contractRepository = contractRepository;
|
|
15
15
|
this.running = false;
|
|
@@ -17,8 +17,10 @@ class MessageBus {
|
|
|
17
17
|
this.tickInProgress = false;
|
|
18
18
|
this.debug = false;
|
|
19
19
|
this.initialized = false;
|
|
20
|
+
this.boundOnMessage = this.onMessage.bind(this);
|
|
20
21
|
this.handlers = new Map(messageHandlers.map((u) => [u.messageTag, u]));
|
|
21
22
|
this.tickIntervalMs = tickIntervalMs;
|
|
23
|
+
this.messageTimeoutMs = messageTimeoutMs;
|
|
22
24
|
this.debug = debug;
|
|
23
25
|
this.buildServicesFn = buildServices ?? this.buildServices.bind(this);
|
|
24
26
|
}
|
|
@@ -29,7 +31,7 @@ class MessageBus {
|
|
|
29
31
|
if (this.debug)
|
|
30
32
|
console.log("MessageBus starting");
|
|
31
33
|
// Hook message routing
|
|
32
|
-
self.addEventListener("message", this.
|
|
34
|
+
self.addEventListener("message", this.boundOnMessage);
|
|
33
35
|
// activate service worker immediately
|
|
34
36
|
self.addEventListener("install", () => {
|
|
35
37
|
self.skipWaiting();
|
|
@@ -52,7 +54,7 @@ class MessageBus {
|
|
|
52
54
|
self.clearTimeout(this.tickTimeout);
|
|
53
55
|
this.tickTimeout = null;
|
|
54
56
|
}
|
|
55
|
-
self.removeEventListener("message", this.
|
|
57
|
+
self.removeEventListener("message", this.boundOnMessage);
|
|
56
58
|
await Promise.all(Array.from(this.handlers.values()).map((updater) => updater.stop()));
|
|
57
59
|
}
|
|
58
60
|
scheduleNextTick() {
|
|
@@ -78,7 +80,7 @@ class MessageBus {
|
|
|
78
80
|
const now = Date.now();
|
|
79
81
|
for (const updater of this.handlers.values()) {
|
|
80
82
|
try {
|
|
81
|
-
const response = await updater.tick(now);
|
|
83
|
+
const response = await this.withTimeout(updater.tick(now), `${updater.messageTag}:tick`);
|
|
82
84
|
if (this.debug)
|
|
83
85
|
console.log(`[${updater.messageTag}] outgoing tick response:`, response);
|
|
84
86
|
if (response && response.length > 0) {
|
|
@@ -169,12 +171,25 @@ class MessageBus {
|
|
|
169
171
|
throw new Error("Missing privateKey or publicKey in configuration object");
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
|
-
|
|
174
|
+
onMessage(event) {
|
|
175
|
+
// Keep the service worker alive while async work is pending.
|
|
176
|
+
// Without this, the browser may terminate the SW mid-operation,
|
|
177
|
+
// causing all pending responses to be lost silently.
|
|
178
|
+
const promise = this.processMessage(event);
|
|
179
|
+
if (typeof event.waitUntil === "function") {
|
|
180
|
+
event.waitUntil(promise);
|
|
181
|
+
}
|
|
182
|
+
return promise;
|
|
183
|
+
}
|
|
184
|
+
async processMessage(event) {
|
|
173
185
|
const { id, tag, broadcast } = event.data;
|
|
174
186
|
if (tag === "INITIALIZE_MESSAGE_BUS") {
|
|
175
187
|
if (this.debug) {
|
|
176
188
|
console.log("Init Command received");
|
|
177
189
|
}
|
|
190
|
+
// Intentionally not wrapped with withTimeout: initialization
|
|
191
|
+
// performs network calls (buildServices) and handler startup
|
|
192
|
+
// that may legitimately exceed the message timeout.
|
|
178
193
|
await this.waitForInit(event.data.config);
|
|
179
194
|
event.source?.postMessage({ id, tag });
|
|
180
195
|
if (this.debug) {
|
|
@@ -208,7 +223,7 @@ class MessageBus {
|
|
|
208
223
|
}
|
|
209
224
|
if (broadcast) {
|
|
210
225
|
const updaters = Array.from(this.handlers.values());
|
|
211
|
-
const results = await Promise.allSettled(updaters.map((updater) => updater.handleMessage(event.data)));
|
|
226
|
+
const results = await Promise.allSettled(updaters.map((updater) => this.withTimeout(updater.handleMessage(event.data), updater.messageTag)));
|
|
212
227
|
results.forEach((result, index) => {
|
|
213
228
|
const updater = updaters[index];
|
|
214
229
|
if (result.status === "fulfilled") {
|
|
@@ -239,7 +254,7 @@ class MessageBus {
|
|
|
239
254
|
return;
|
|
240
255
|
}
|
|
241
256
|
try {
|
|
242
|
-
const response = await updater.handleMessage(event.data);
|
|
257
|
+
const response = await this.withTimeout(updater.handleMessage(event.data), tag);
|
|
243
258
|
if (this.debug)
|
|
244
259
|
console.log(`[${tag}] outgoing response:`, response);
|
|
245
260
|
if (response) {
|
|
@@ -253,6 +268,27 @@ class MessageBus {
|
|
|
253
268
|
event.source?.postMessage({ id, tag, error });
|
|
254
269
|
}
|
|
255
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Race `promise` against a timeout. Note: this does NOT cancel the
|
|
273
|
+
* underlying work — the original promise keeps running. This is safe
|
|
274
|
+
* here because only the caller (not the handler) posts the response.
|
|
275
|
+
*/
|
|
276
|
+
withTimeout(promise, label) {
|
|
277
|
+
if (this.messageTimeoutMs <= 0)
|
|
278
|
+
return promise;
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
const timer = self.setTimeout(() => {
|
|
281
|
+
reject(new Error(`Message handler timed out after ${this.messageTimeoutMs}ms (${label})`));
|
|
282
|
+
}, this.messageTimeoutMs);
|
|
283
|
+
promise.then((val) => {
|
|
284
|
+
self.clearTimeout(timer);
|
|
285
|
+
resolve(val);
|
|
286
|
+
}, (err) => {
|
|
287
|
+
self.clearTimeout(timer);
|
|
288
|
+
reject(err);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
256
292
|
/**
|
|
257
293
|
* Returns the registered SW for the path.
|
|
258
294
|
* It uses the functions in `service-worker-manager.ts` module.
|
|
@@ -4,6 +4,8 @@ import { scriptFromTapLeafScript } from '../script/base.js';
|
|
|
4
4
|
import { buildForfeitTxWithOutput } from '../forfeit.js';
|
|
5
5
|
import { Address, OutScript, SigHash } from "@scure/btc-signer";
|
|
6
6
|
import { getNetwork } from '../networks.js';
|
|
7
|
+
import { createAssetPacket } from './asset.js';
|
|
8
|
+
import { Extension } from '../extension/index.js';
|
|
7
9
|
export class DelegatorManagerImpl {
|
|
8
10
|
constructor(delegatorProvider, arkInfoProvider, identity) {
|
|
9
11
|
this.delegatorProvider = delegatorProvider;
|
|
@@ -213,6 +215,48 @@ async function makeDelegateForfeitTx(input, connectorAmount, delegatePubkey, for
|
|
|
213
215
|
return identity.sign(tx);
|
|
214
216
|
}
|
|
215
217
|
async function makeSignedDelegateIntent(identity, coins, outputs, onchainOutputsIndexes, cosignerPubKeys, validAt) {
|
|
218
|
+
// if some of the inputs hold assets, build the asset packet and append as output
|
|
219
|
+
// in the intent proof tx, there is a "fake" input at index 0
|
|
220
|
+
// so the real coin indices are offset by +1
|
|
221
|
+
const assetInputs = new Map();
|
|
222
|
+
for (let i = 0; i < coins.length; i++) {
|
|
223
|
+
if ("assets" in coins[i]) {
|
|
224
|
+
const assets = coins[i].assets;
|
|
225
|
+
if (assets && assets.length > 0) {
|
|
226
|
+
assetInputs.set(i + 1, assets);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let outputAssets;
|
|
231
|
+
let assetOutputIndex; // where to send the assets
|
|
232
|
+
if (assetInputs.size > 0) {
|
|
233
|
+
// collect all input assets and assign them to the first offchain output
|
|
234
|
+
const allAssets = new Map();
|
|
235
|
+
for (const [, assets] of assetInputs) {
|
|
236
|
+
for (const asset of assets) {
|
|
237
|
+
const existing = allAssets.get(asset.assetId) ?? 0n;
|
|
238
|
+
allAssets.set(asset.assetId, existing + BigInt(asset.amount));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
outputAssets = [];
|
|
242
|
+
for (const [assetId, amount] of allAssets) {
|
|
243
|
+
outputAssets.push({ assetId, amount: Number(amount) });
|
|
244
|
+
}
|
|
245
|
+
const firstOffchainIndex = outputs.findIndex((_, i) => !onchainOutputsIndexes.includes(i));
|
|
246
|
+
if (firstOffchainIndex === -1) {
|
|
247
|
+
throw new Error("Cannot settle assets without an offchain output");
|
|
248
|
+
}
|
|
249
|
+
assetOutputIndex = firstOffchainIndex;
|
|
250
|
+
}
|
|
251
|
+
const recipients = outputs.map((output, i) => ({
|
|
252
|
+
address: "", // not needed for asset packet creation
|
|
253
|
+
amount: Number(output.amount),
|
|
254
|
+
assets: i === assetOutputIndex ? outputAssets : undefined,
|
|
255
|
+
}));
|
|
256
|
+
if (outputAssets && outputAssets.length > 0) {
|
|
257
|
+
const assetPacket = createAssetPacket(assetInputs, recipients);
|
|
258
|
+
outputs.push(Extension.create([assetPacket]).txOut());
|
|
259
|
+
}
|
|
216
260
|
const message = {
|
|
217
261
|
type: "register",
|
|
218
262
|
onchain_output_indexes: onchainOutputsIndexes,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
|
-
import { runTasks } from '../../worker/expo/taskRunner.js';
|
|
2
|
+
import { runTasks, createTaskDependencies } from '../../worker/expo/taskRunner.js';
|
|
3
3
|
import { contractPollProcessor, CONTRACT_POLL_TASK_TYPE, } from '../../worker/expo/processors/index.js';
|
|
4
4
|
import { DefaultVtxo } from '../../script/default.js';
|
|
5
5
|
import { ExpoArkProvider } from '../../providers/expoArk.js';
|
|
6
6
|
import { ExpoIndexerProvider } from '../../providers/expoIndexer.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getRandomId } from '../utils.js';
|
|
8
8
|
function requireTaskManager() {
|
|
9
9
|
try {
|
|
10
10
|
return require("expo-task-manager");
|
|
@@ -61,7 +61,7 @@ export function defineExpoBackgroundTask(taskName, options) {
|
|
|
61
61
|
const arkProvider = new ExpoArkProvider(config.arkServerUrl);
|
|
62
62
|
// Reconstruct default offchainTapscript as fallback
|
|
63
63
|
// for VTXOs not associated with a contract.
|
|
64
|
-
const
|
|
64
|
+
const offchainTapscript = new DefaultVtxo.Script({
|
|
65
65
|
pubKey: hex.decode(config.pubkeyHex),
|
|
66
66
|
serverPubKey: hex.decode(config.serverPubKeyHex),
|
|
67
67
|
csvTimelock: {
|
|
@@ -69,18 +69,14 @@ export function defineExpoBackgroundTask(taskName, options) {
|
|
|
69
69
|
type: config.exitTimelockType,
|
|
70
70
|
},
|
|
71
71
|
});
|
|
72
|
-
|
|
72
|
+
const deps = createTaskDependencies({
|
|
73
73
|
walletRepository,
|
|
74
74
|
contractRepository,
|
|
75
75
|
indexerProvider,
|
|
76
76
|
arkProvider,
|
|
77
|
-
|
|
78
|
-
if (contract) {
|
|
79
|
-
return extendVtxoFromContract(vtxo, contract);
|
|
80
|
-
}
|
|
81
|
-
return extendVirtualCoin({ offchainTapscript: defaultTapscript }, vtxo);
|
|
82
|
-
},
|
|
77
|
+
offchainTapscript,
|
|
83
78
|
});
|
|
79
|
+
await runTasks(taskQueue, processors, deps);
|
|
84
80
|
// Acknowledge outbox results (no foreground to consume them)
|
|
85
81
|
const results = await taskQueue.getResults();
|
|
86
82
|
if (results.length > 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { InMemoryTaskQueue } from './taskQueue.js';
|
|
2
2
|
export { AsyncStorageTaskQueue } from './asyncStorageTaskQueue.js';
|
|
3
|
-
export { runTasks } from './taskRunner.js';
|
|
3
|
+
export { runTasks, createTaskDependencies } from './taskRunner.js';
|
|
4
4
|
export { contractPollProcessor, CONTRACT_POLL_TASK_TYPE } from './processors/index.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRandomId } from '../../wallet/utils.js';
|
|
1
|
+
import { getRandomId, extendVirtualCoin, extendVtxoFromContract, } from '../../wallet/utils.js';
|
|
2
2
|
/**
|
|
3
3
|
* Run all pending tasks from the queue through matching processors.
|
|
4
4
|
*
|
|
@@ -52,3 +52,27 @@ export async function runTasks(queue, processors, deps) {
|
|
|
52
52
|
}
|
|
53
53
|
return results;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the {@link TaskDependencies} needed by task processors
|
|
57
|
+
* (e.g. {@link import("./processors").contractPollProcessor}).
|
|
58
|
+
*
|
|
59
|
+
* This is the same construction that `defineExpoBackgroundTask` does
|
|
60
|
+
* internally, extracted so that consumers with custom schedulers
|
|
61
|
+
* (e.g. bare React Native with `react-native-background-fetch`)
|
|
62
|
+
* can build deps without depending on Expo.
|
|
63
|
+
*/
|
|
64
|
+
export function createTaskDependencies(options) {
|
|
65
|
+
const { walletRepository, contractRepository, indexerProvider, arkProvider, offchainTapscript, } = options;
|
|
66
|
+
return {
|
|
67
|
+
walletRepository,
|
|
68
|
+
contractRepository,
|
|
69
|
+
indexerProvider,
|
|
70
|
+
arkProvider,
|
|
71
|
+
extendVtxo: (vtxo, contract) => {
|
|
72
|
+
if (contract) {
|
|
73
|
+
return extendVtxoFromContract(vtxo, contract);
|
|
74
|
+
}
|
|
75
|
+
return extendVirtualCoin({ offchainTapscript }, vtxo);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -6,7 +6,7 @@ import { ReadonlySingleKey, SingleKey } from '../identity/index.js';
|
|
|
6
6
|
import { ReadonlyWallet, Wallet } from '../wallet/wallet.js';
|
|
7
7
|
import { hex } from "@scure/base";
|
|
8
8
|
export class MessageBus {
|
|
9
|
-
constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, debug = false, buildServices, }) {
|
|
9
|
+
constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, messageTimeoutMs = 30000, debug = false, buildServices, }) {
|
|
10
10
|
this.walletRepository = walletRepository;
|
|
11
11
|
this.contractRepository = contractRepository;
|
|
12
12
|
this.running = false;
|
|
@@ -14,8 +14,10 @@ export class MessageBus {
|
|
|
14
14
|
this.tickInProgress = false;
|
|
15
15
|
this.debug = false;
|
|
16
16
|
this.initialized = false;
|
|
17
|
+
this.boundOnMessage = this.onMessage.bind(this);
|
|
17
18
|
this.handlers = new Map(messageHandlers.map((u) => [u.messageTag, u]));
|
|
18
19
|
this.tickIntervalMs = tickIntervalMs;
|
|
20
|
+
this.messageTimeoutMs = messageTimeoutMs;
|
|
19
21
|
this.debug = debug;
|
|
20
22
|
this.buildServicesFn = buildServices ?? this.buildServices.bind(this);
|
|
21
23
|
}
|
|
@@ -26,7 +28,7 @@ export class MessageBus {
|
|
|
26
28
|
if (this.debug)
|
|
27
29
|
console.log("MessageBus starting");
|
|
28
30
|
// Hook message routing
|
|
29
|
-
self.addEventListener("message", this.
|
|
31
|
+
self.addEventListener("message", this.boundOnMessage);
|
|
30
32
|
// activate service worker immediately
|
|
31
33
|
self.addEventListener("install", () => {
|
|
32
34
|
self.skipWaiting();
|
|
@@ -49,7 +51,7 @@ export class MessageBus {
|
|
|
49
51
|
self.clearTimeout(this.tickTimeout);
|
|
50
52
|
this.tickTimeout = null;
|
|
51
53
|
}
|
|
52
|
-
self.removeEventListener("message", this.
|
|
54
|
+
self.removeEventListener("message", this.boundOnMessage);
|
|
53
55
|
await Promise.all(Array.from(this.handlers.values()).map((updater) => updater.stop()));
|
|
54
56
|
}
|
|
55
57
|
scheduleNextTick() {
|
|
@@ -75,7 +77,7 @@ export class MessageBus {
|
|
|
75
77
|
const now = Date.now();
|
|
76
78
|
for (const updater of this.handlers.values()) {
|
|
77
79
|
try {
|
|
78
|
-
const response = await updater.tick(now);
|
|
80
|
+
const response = await this.withTimeout(updater.tick(now), `${updater.messageTag}:tick`);
|
|
79
81
|
if (this.debug)
|
|
80
82
|
console.log(`[${updater.messageTag}] outgoing tick response:`, response);
|
|
81
83
|
if (response && response.length > 0) {
|
|
@@ -166,12 +168,25 @@ export class MessageBus {
|
|
|
166
168
|
throw new Error("Missing privateKey or publicKey in configuration object");
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
|
-
|
|
171
|
+
onMessage(event) {
|
|
172
|
+
// Keep the service worker alive while async work is pending.
|
|
173
|
+
// Without this, the browser may terminate the SW mid-operation,
|
|
174
|
+
// causing all pending responses to be lost silently.
|
|
175
|
+
const promise = this.processMessage(event);
|
|
176
|
+
if (typeof event.waitUntil === "function") {
|
|
177
|
+
event.waitUntil(promise);
|
|
178
|
+
}
|
|
179
|
+
return promise;
|
|
180
|
+
}
|
|
181
|
+
async processMessage(event) {
|
|
170
182
|
const { id, tag, broadcast } = event.data;
|
|
171
183
|
if (tag === "INITIALIZE_MESSAGE_BUS") {
|
|
172
184
|
if (this.debug) {
|
|
173
185
|
console.log("Init Command received");
|
|
174
186
|
}
|
|
187
|
+
// Intentionally not wrapped with withTimeout: initialization
|
|
188
|
+
// performs network calls (buildServices) and handler startup
|
|
189
|
+
// that may legitimately exceed the message timeout.
|
|
175
190
|
await this.waitForInit(event.data.config);
|
|
176
191
|
event.source?.postMessage({ id, tag });
|
|
177
192
|
if (this.debug) {
|
|
@@ -205,7 +220,7 @@ export class MessageBus {
|
|
|
205
220
|
}
|
|
206
221
|
if (broadcast) {
|
|
207
222
|
const updaters = Array.from(this.handlers.values());
|
|
208
|
-
const results = await Promise.allSettled(updaters.map((updater) => updater.handleMessage(event.data)));
|
|
223
|
+
const results = await Promise.allSettled(updaters.map((updater) => this.withTimeout(updater.handleMessage(event.data), updater.messageTag)));
|
|
209
224
|
results.forEach((result, index) => {
|
|
210
225
|
const updater = updaters[index];
|
|
211
226
|
if (result.status === "fulfilled") {
|
|
@@ -236,7 +251,7 @@ export class MessageBus {
|
|
|
236
251
|
return;
|
|
237
252
|
}
|
|
238
253
|
try {
|
|
239
|
-
const response = await updater.handleMessage(event.data);
|
|
254
|
+
const response = await this.withTimeout(updater.handleMessage(event.data), tag);
|
|
240
255
|
if (this.debug)
|
|
241
256
|
console.log(`[${tag}] outgoing response:`, response);
|
|
242
257
|
if (response) {
|
|
@@ -250,6 +265,27 @@ export class MessageBus {
|
|
|
250
265
|
event.source?.postMessage({ id, tag, error });
|
|
251
266
|
}
|
|
252
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Race `promise` against a timeout. Note: this does NOT cancel the
|
|
270
|
+
* underlying work — the original promise keeps running. This is safe
|
|
271
|
+
* here because only the caller (not the handler) posts the response.
|
|
272
|
+
*/
|
|
273
|
+
withTimeout(promise, label) {
|
|
274
|
+
if (this.messageTimeoutMs <= 0)
|
|
275
|
+
return promise;
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
const timer = self.setTimeout(() => {
|
|
278
|
+
reject(new Error(`Message handler timed out after ${this.messageTimeoutMs}ms (${label})`));
|
|
279
|
+
}, this.messageTimeoutMs);
|
|
280
|
+
promise.then((val) => {
|
|
281
|
+
self.clearTimeout(timer);
|
|
282
|
+
resolve(val);
|
|
283
|
+
}, (err) => {
|
|
284
|
+
self.clearTimeout(timer);
|
|
285
|
+
reject(err);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
253
289
|
/**
|
|
254
290
|
* Returns the registered SW for the path.
|
|
255
291
|
* It uses the functions in `service-worker-manager.ts` module.
|
|
@@ -2,6 +2,6 @@ export type { TaskItem, TaskResult, TaskQueue } from "./taskQueue";
|
|
|
2
2
|
export { InMemoryTaskQueue } from "./taskQueue";
|
|
3
3
|
export type { AsyncStorageLike } from "./asyncStorageTaskQueue";
|
|
4
4
|
export { AsyncStorageTaskQueue } from "./asyncStorageTaskQueue";
|
|
5
|
-
export type { TaskProcessor, TaskDependencies } from "./taskRunner";
|
|
6
|
-
export { runTasks } from "./taskRunner";
|
|
5
|
+
export type { TaskProcessor, TaskDependencies, CreateTaskDependenciesOptions, } from "./taskRunner";
|
|
6
|
+
export { runTasks, createTaskDependencies } from "./taskRunner";
|
|
7
7
|
export { contractPollProcessor, CONTRACT_POLL_TASK_TYPE } from "./processors";
|
|
@@ -5,6 +5,7 @@ import type { IndexerProvider } from "../../providers/indexer";
|
|
|
5
5
|
import type { ArkProvider } from "../../providers/ark";
|
|
6
6
|
import type { ExtendedVirtualCoin, VirtualCoin } from "../../wallet";
|
|
7
7
|
import type { Contract } from "../../contracts/types";
|
|
8
|
+
import type { ReadonlyWallet } from "../../wallet/wallet";
|
|
8
9
|
/**
|
|
9
10
|
* Shared dependencies injected into every processor at runtime.
|
|
10
11
|
*/
|
|
@@ -40,3 +41,23 @@ export interface TaskProcessor<TDeps = TaskDependencies> {
|
|
|
40
41
|
* Processor errors produce a `"failed"` result with the error message.
|
|
41
42
|
*/
|
|
42
43
|
export declare function runTasks<TDeps = TaskDependencies>(queue: TaskQueue, processors: TaskProcessor<TDeps>[], deps: TDeps): Promise<TaskResult[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Options for {@link createTaskDependencies}.
|
|
46
|
+
*/
|
|
47
|
+
export interface CreateTaskDependenciesOptions {
|
|
48
|
+
walletRepository: WalletRepository;
|
|
49
|
+
contractRepository: ContractRepository;
|
|
50
|
+
indexerProvider: IndexerProvider;
|
|
51
|
+
arkProvider: ArkProvider;
|
|
52
|
+
offchainTapscript: ReadonlyWallet["offchainTapscript"];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build the {@link TaskDependencies} needed by task processors
|
|
56
|
+
* (e.g. {@link import("./processors").contractPollProcessor}).
|
|
57
|
+
*
|
|
58
|
+
* This is the same construction that `defineExpoBackgroundTask` does
|
|
59
|
+
* internally, extracted so that consumers with custom schedulers
|
|
60
|
+
* (e.g. bare React Native with `react-native-background-fetch`)
|
|
61
|
+
* can build deps without depending on Expo.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createTaskDependencies(options: CreateTaskDependenciesOptions): TaskDependencies;
|
|
@@ -48,6 +48,7 @@ export interface MessageHandler<REQ extends RequestEnvelope = RequestEnvelope, R
|
|
|
48
48
|
type Options = {
|
|
49
49
|
messageHandlers: MessageHandler[];
|
|
50
50
|
tickIntervalMs?: number;
|
|
51
|
+
messageTimeoutMs?: number;
|
|
51
52
|
debug?: boolean;
|
|
52
53
|
buildServices?: (config: Initialize["config"]) => Promise<{
|
|
53
54
|
arkProvider: ArkProvider;
|
|
@@ -76,13 +77,15 @@ export declare class MessageBus {
|
|
|
76
77
|
private readonly contractRepository;
|
|
77
78
|
private handlers;
|
|
78
79
|
private tickIntervalMs;
|
|
80
|
+
private messageTimeoutMs;
|
|
79
81
|
private running;
|
|
80
82
|
private tickTimeout;
|
|
81
83
|
private tickInProgress;
|
|
82
84
|
private debug;
|
|
83
85
|
private initialized;
|
|
84
86
|
private readonly buildServicesFn;
|
|
85
|
-
|
|
87
|
+
private readonly boundOnMessage;
|
|
88
|
+
constructor(walletRepository: WalletRepository, contractRepository: ContractRepository, { messageHandlers, tickIntervalMs, messageTimeoutMs, debug, buildServices, }: Options);
|
|
86
89
|
start(): Promise<void>;
|
|
87
90
|
stop(): Promise<void>;
|
|
88
91
|
private scheduleNextTick;
|
|
@@ -90,6 +93,13 @@ export declare class MessageBus {
|
|
|
90
93
|
private waitForInit;
|
|
91
94
|
private buildServices;
|
|
92
95
|
private onMessage;
|
|
96
|
+
private processMessage;
|
|
97
|
+
/**
|
|
98
|
+
* Race `promise` against a timeout. Note: this does NOT cancel the
|
|
99
|
+
* underlying work — the original promise keeps running. This is safe
|
|
100
|
+
* here because only the caller (not the handler) posts the response.
|
|
101
|
+
*/
|
|
102
|
+
private withTimeout;
|
|
93
103
|
/**
|
|
94
104
|
* Returns the registered SW for the path.
|
|
95
105
|
* It uses the functions in `service-worker-manager.ts` module.
|