@arkade-os/sdk 0.4.0-next.2 → 0.4.0-next.4
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/{asset → extension/asset}/index.js +1 -2
- package/dist/cjs/extension/asset/packet.js +111 -0
- package/dist/cjs/{asset → extension/asset}/types.js +1 -4
- package/dist/cjs/extension/index.js +254 -0
- package/dist/cjs/extension/packet.js +20 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/providers/ark.js +52 -37
- package/dist/cjs/providers/indexer.js +1 -1
- package/dist/cjs/providers/utils.js +39 -29
- package/dist/cjs/wallet/asset-manager.js +9 -17
- package/dist/cjs/wallet/asset.js +1 -1
- package/dist/cjs/wallet/validation.js +6 -2
- package/dist/cjs/wallet/wallet.js +5 -4
- package/dist/cjs/worker/browser/service-worker-manager.js +111 -10
- package/dist/esm/{asset → extension/asset}/index.js +1 -1
- package/dist/esm/extension/asset/packet.js +107 -0
- package/dist/esm/{asset → extension/asset}/types.js +0 -3
- package/dist/esm/extension/index.js +248 -0
- package/dist/esm/extension/packet.js +16 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/providers/ark.js +52 -37
- package/dist/esm/providers/indexer.js +1 -1
- package/dist/esm/providers/utils.js +39 -29
- package/dist/esm/wallet/asset-manager.js +9 -17
- package/dist/esm/wallet/asset.js +1 -1
- package/dist/esm/wallet/validation.js +6 -2
- package/dist/esm/wallet/wallet.js +5 -4
- package/dist/esm/worker/browser/service-worker-manager.js +111 -10
- package/dist/types/{asset → extension/asset}/index.d.ts +1 -1
- package/dist/types/extension/asset/packet.d.ts +38 -0
- package/dist/types/{asset → extension/asset}/types.d.ts +0 -2
- package/dist/types/extension/index.d.ts +56 -0
- package/dist/types/extension/packet.d.ts +21 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/providers/utils.d.ts +6 -0
- package/dist/types/wallet/asset-manager.d.ts +4 -13
- package/dist/types/wallet/asset.d.ts +1 -1
- package/dist/types/worker/browser/service-worker-manager.d.ts +15 -4
- package/package.json +1 -1
- package/dist/cjs/asset/packet.js +0 -164
- package/dist/esm/asset/packet.js +0 -159
- package/dist/types/asset/packet.d.ts +0 -27
- /package/dist/cjs/{asset → extension/asset}/assetGroup.js +0 -0
- /package/dist/cjs/{asset → extension/asset}/assetId.js +0 -0
- /package/dist/cjs/{asset → extension/asset}/assetInput.js +0 -0
- /package/dist/cjs/{asset → extension/asset}/assetOutput.js +0 -0
- /package/dist/cjs/{asset → extension/asset}/assetRef.js +0 -0
- /package/dist/cjs/{asset → extension/asset}/metadata.js +0 -0
- /package/dist/cjs/{asset → extension/asset}/utils.js +0 -0
- /package/dist/esm/{asset → extension/asset}/assetGroup.js +0 -0
- /package/dist/esm/{asset → extension/asset}/assetId.js +0 -0
- /package/dist/esm/{asset → extension/asset}/assetInput.js +0 -0
- /package/dist/esm/{asset → extension/asset}/assetOutput.js +0 -0
- /package/dist/esm/{asset → extension/asset}/assetRef.js +0 -0
- /package/dist/esm/{asset → extension/asset}/metadata.js +0 -0
- /package/dist/esm/{asset → extension/asset}/utils.js +0 -0
- /package/dist/types/{asset → extension/asset}/assetGroup.d.ts +0 -0
- /package/dist/types/{asset → extension/asset}/assetId.d.ts +0 -0
- /package/dist/types/{asset → extension/asset}/assetInput.d.ts +0 -0
- /package/dist/types/{asset → extension/asset}/assetOutput.d.ts +0 -0
- /package/dist/types/{asset → extension/asset}/assetRef.d.ts +0 -0
- /package/dist/types/{asset → extension/asset}/metadata.d.ts +0 -0
- /package/dist/types/{asset → extension/asset}/utils.d.ts +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AssetManager = exports.ReadonlyAssetManager = void 0;
|
|
4
|
-
const asset_1 = require("../asset");
|
|
4
|
+
const asset_1 = require("../extension/asset");
|
|
5
5
|
const address_1 = require("../script/address");
|
|
6
6
|
const asset_2 = require("./asset");
|
|
7
|
+
const extension_1 = require("../extension");
|
|
7
8
|
const wallet_1 = require("./wallet");
|
|
8
9
|
class ReadonlyAssetManager {
|
|
9
10
|
constructor(indexer) {
|
|
@@ -24,30 +25,21 @@ class AssetManager extends ReadonlyAssetManager {
|
|
|
24
25
|
* Issue a new asset.
|
|
25
26
|
* @param params - Parameters for asset issuance
|
|
26
27
|
* @param params.amount - Amount of asset units to issue
|
|
27
|
-
* @param params.
|
|
28
|
+
* @param params.controlAssetId - Optional control asset ID (for reissuable assets)
|
|
28
29
|
* @param params.metadata - Optional metadata to attach to the asset
|
|
29
30
|
* @returns Promise resolving to the ark transaction ID and asset ID
|
|
30
31
|
*
|
|
31
32
|
* @example
|
|
32
33
|
* ```typescript
|
|
33
34
|
* // Issue a simple non-reissuable asset
|
|
34
|
-
* const result = await wallet.
|
|
35
|
-
* console.log('Asset ID:', result.assetId);
|
|
36
|
-
*
|
|
37
|
-
* // Issue a reissuable asset with a new control asset
|
|
38
|
-
* const result = await wallet.issueAsset({
|
|
39
|
-
* amount: 1000,
|
|
40
|
-
* controlAsset: 1 // creates new control asset with amount 1
|
|
41
|
-
* });
|
|
42
|
-
* console.log('Control Asset ID:', result.controlAssetId);
|
|
35
|
+
* const result = await wallet.assetManager.issue({ amount: 1000 });
|
|
43
36
|
* console.log('Asset ID:', result.assetId);
|
|
44
37
|
*
|
|
45
38
|
* // Issue a reissuable asset with an existing control asset
|
|
46
|
-
* const result = await wallet.
|
|
39
|
+
* const result = await wallet.assetManager.issue({
|
|
47
40
|
* amount: 1000,
|
|
48
|
-
*
|
|
41
|
+
* controlAssetId: 'existingControlAssetId'
|
|
49
42
|
* });
|
|
50
|
-
* console.log('Control Asset ID:', result.controlAssetId);
|
|
51
43
|
* console.log('Asset ID:', result.assetId);
|
|
52
44
|
* ```
|
|
53
45
|
*/
|
|
@@ -105,7 +97,7 @@ class AssetManager extends ReadonlyAssetManager {
|
|
|
105
97
|
script: outputAddress.pkScript,
|
|
106
98
|
amount: BigInt(totalBtcSelected),
|
|
107
99
|
},
|
|
108
|
-
asset_1.Packet.create(groups).txOut(),
|
|
100
|
+
extension_1.Extension.create([asset_1.Packet.create(groups)]).txOut(),
|
|
109
101
|
];
|
|
110
102
|
const { arkTxid } = await this.wallet.buildAndSubmitOffchainTx(coinSelection.inputs, outputs);
|
|
111
103
|
return {
|
|
@@ -217,7 +209,7 @@ class AssetManager extends ReadonlyAssetManager {
|
|
|
217
209
|
script: outputAddress.pkScript,
|
|
218
210
|
amount: BigInt(totalBtcSelected),
|
|
219
211
|
},
|
|
220
|
-
asset_1.Packet.create(groups).txOut(),
|
|
212
|
+
extension_1.Extension.create([asset_1.Packet.create(groups)]).txOut(),
|
|
221
213
|
];
|
|
222
214
|
const { arkTxid } = await this.wallet.buildAndSubmitOffchainTx(selectedCoins, outputs);
|
|
223
215
|
return arkTxid;
|
|
@@ -301,7 +293,7 @@ class AssetManager extends ReadonlyAssetManager {
|
|
|
301
293
|
script: outputAddress.pkScript,
|
|
302
294
|
amount: BigInt(totalBtcSelected),
|
|
303
295
|
},
|
|
304
|
-
asset_1.Packet.create(groups).txOut(),
|
|
296
|
+
extension_1.Extension.create([asset_1.Packet.create(groups)]).txOut(),
|
|
305
297
|
];
|
|
306
298
|
const { arkTxid } = await this.wallet.buildAndSubmitOffchainTx(selectedCoins, outputs);
|
|
307
299
|
return arkTxid;
|
package/dist/cjs/wallet/asset.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.createAssetPacket = createAssetPacket;
|
|
|
4
4
|
exports.selectCoinsWithAsset = selectCoinsWithAsset;
|
|
5
5
|
exports.computeAssetChange = computeAssetChange;
|
|
6
6
|
exports.selectedCoinsToAssetInputs = selectedCoinsToAssetInputs;
|
|
7
|
-
const asset_1 = require("../asset");
|
|
7
|
+
const asset_1 = require("../extension/asset");
|
|
8
8
|
/**
|
|
9
9
|
* Creates an asset packet from asset inputs and receivers.
|
|
10
10
|
* Groups inputs and outputs by asset ID and creates the Packet object
|
|
@@ -4,7 +4,7 @@ exports.ErrInvalidOffchainOutputAmount = exports.ErrOnchainOutputNotFound = expo
|
|
|
4
4
|
exports.validateBatchRecipients = validateBatchRecipients;
|
|
5
5
|
const utils_js_1 = require("@scure/btc-signer/utils.js");
|
|
6
6
|
const address_1 = require("../script/address");
|
|
7
|
-
const
|
|
7
|
+
const extension_1 = require("../extension");
|
|
8
8
|
const btc_signer_1 = require("@scure/btc-signer");
|
|
9
9
|
const ErrOffchainOutputNotFound = (address) => new Error(`offchain send output not found: ${address}`);
|
|
10
10
|
exports.ErrOffchainOutputNotFound = ErrOffchainOutputNotFound;
|
|
@@ -125,7 +125,11 @@ function validateOffchainRecipient(leaves, arkAddress, recipient, usedOutputs //
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
function validateAssetOutputs(leafTx, outputIndex, expectedAssets) {
|
|
128
|
-
const
|
|
128
|
+
const ext = extension_1.Extension.fromTx(leafTx);
|
|
129
|
+
const assetPacket = ext.getAssetPacket();
|
|
130
|
+
if (!assetPacket) {
|
|
131
|
+
throw new Error("no asset packet found in extension");
|
|
132
|
+
}
|
|
129
133
|
for (const { assetId, amount } of expectedAssets) {
|
|
130
134
|
validateAssetGroupOutput(assetPacket, outputIndex, assetId, amount);
|
|
131
135
|
}
|
|
@@ -30,6 +30,7 @@ const batch_1 = require("./batch");
|
|
|
30
30
|
const arkfee_1 = require("../arkfee");
|
|
31
31
|
const transactionHistory_1 = require("../utils/transactionHistory");
|
|
32
32
|
const asset_manager_1 = require("./asset-manager");
|
|
33
|
+
const extension_1 = require("../extension");
|
|
33
34
|
const delegate_1 = require("../script/delegate");
|
|
34
35
|
const delegator_1 = require("./delegator");
|
|
35
36
|
const repositories_1 = require("../repositories");
|
|
@@ -918,7 +919,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
918
919
|
}));
|
|
919
920
|
if (outputAssets && outputAssets.length > 0) {
|
|
920
921
|
const assetPacket = (0, asset_1.createAssetPacket)(assetInputs, recipients);
|
|
921
|
-
outputs.push(assetPacket.txOut());
|
|
922
|
+
outputs.push(extension_1.Extension.create([assetPacket]).txOut());
|
|
922
923
|
}
|
|
923
924
|
// session holds the state of the musig2 signing process of the vtxo tree
|
|
924
925
|
let session;
|
|
@@ -931,15 +932,15 @@ class Wallet extends ReadonlyWallet {
|
|
|
931
932
|
this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
|
|
932
933
|
this.makeDeleteIntentSignature(params.inputs),
|
|
933
934
|
]);
|
|
934
|
-
const intentId = await this.safeRegisterIntent(intent);
|
|
935
935
|
const topics = [
|
|
936
936
|
...signingPublicKeys,
|
|
937
937
|
...params.inputs.map((input) => `${input.txid}:${input.vout}`),
|
|
938
938
|
];
|
|
939
|
-
const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
|
|
940
939
|
const abortController = new AbortController();
|
|
941
940
|
try {
|
|
942
941
|
const stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
942
|
+
const intentId = await this.safeRegisterIntent(intent);
|
|
943
|
+
const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
|
|
943
944
|
const commitmentTxid = await batch_1.Batch.join(stream, handler, {
|
|
944
945
|
abortController,
|
|
945
946
|
skipVtxoTreeSigning: !hasOffchainOutputs,
|
|
@@ -1404,7 +1405,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1404
1405
|
recipients.some((r) => r.assets && r.assets.length > 0);
|
|
1405
1406
|
if (hasAssets) {
|
|
1406
1407
|
const assetPacket = (0, asset_1.createAssetPacket)(assetInputs, recipients, changeReceiver);
|
|
1407
|
-
outputs.push(assetPacket.txOut());
|
|
1408
|
+
outputs.push(extension_1.Extension.create([assetPacket]).txOut());
|
|
1408
1409
|
}
|
|
1409
1410
|
const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
|
|
1410
1411
|
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selectedCoins, outputs);
|
|
@@ -4,15 +4,111 @@ exports.__resetServiceWorkerManager = void 0;
|
|
|
4
4
|
exports.setupServiceWorkerOnce = setupServiceWorkerOnce;
|
|
5
5
|
exports.getActiveServiceWorker = getActiveServiceWorker;
|
|
6
6
|
const registrations = new Map();
|
|
7
|
+
let handshakes = new WeakSet();
|
|
7
8
|
function ensureServiceWorkerSupport() {
|
|
8
9
|
if (!("serviceWorker" in navigator)) {
|
|
9
10
|
throw new Error("Service workers are not supported in this browser");
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
|
-
function
|
|
13
|
+
function debugLog(debug, ...args) {
|
|
14
|
+
if (debug) {
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
console.debug(...args);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function normalizeOptions(pathOrOptions) {
|
|
20
|
+
if (typeof pathOrOptions === "string") {
|
|
21
|
+
return {
|
|
22
|
+
path: pathOrOptions,
|
|
23
|
+
updateViaCache: "none",
|
|
24
|
+
autoReload: true,
|
|
25
|
+
debug: false,
|
|
26
|
+
activationTimeoutMs: 10000,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
path: pathOrOptions.path,
|
|
31
|
+
updateViaCache: pathOrOptions.updateViaCache ?? "none",
|
|
32
|
+
autoReload: pathOrOptions.autoReload ?? true,
|
|
33
|
+
onNeedRefresh: pathOrOptions.onNeedRefresh,
|
|
34
|
+
onUpdated: pathOrOptions.onUpdated,
|
|
35
|
+
debug: pathOrOptions.debug ?? false,
|
|
36
|
+
activationTimeoutMs: pathOrOptions.activationTimeoutMs ?? 10000,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function sendSkipWaiting(worker, debug) {
|
|
40
|
+
if (!worker)
|
|
41
|
+
return;
|
|
42
|
+
try {
|
|
43
|
+
worker.postMessage({ type: "SKIP_WAITING" });
|
|
44
|
+
debugLog(debug, "Sent SKIP_WAITING to waiting service worker");
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.warn("Failed to post SKIP_WAITING to service worker", error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function attachUpdateHandlers(registration, options) {
|
|
51
|
+
// Guard: only the first caller per registration attaches handlers.
|
|
52
|
+
// Subsequent calls with different options are silently ignored.
|
|
53
|
+
if (handshakes.has(registration))
|
|
54
|
+
return;
|
|
55
|
+
handshakes.add(registration);
|
|
56
|
+
const { autoReload, onNeedRefresh, onUpdated, activationTimeoutMs, debug } = options;
|
|
57
|
+
let reloadTriggered = false;
|
|
58
|
+
const maybeReload = () => {
|
|
59
|
+
if (reloadTriggered)
|
|
60
|
+
return;
|
|
61
|
+
reloadTriggered = true;
|
|
62
|
+
debugLog(debug, "Service worker controller change detected");
|
|
63
|
+
onUpdated?.();
|
|
64
|
+
if (autoReload &&
|
|
65
|
+
typeof window !== "undefined" &&
|
|
66
|
+
typeof window.location?.reload === "function") {
|
|
67
|
+
window.location.reload();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const handleWaiting = (worker) => {
|
|
71
|
+
if (!worker)
|
|
72
|
+
return;
|
|
73
|
+
onNeedRefresh?.();
|
|
74
|
+
sendSkipWaiting(worker, debug);
|
|
75
|
+
if (activationTimeoutMs > 0 && typeof window !== "undefined") {
|
|
76
|
+
window.setTimeout(() => {
|
|
77
|
+
if (registration.waiting) {
|
|
78
|
+
debugLog(debug, "Waiting worker still pending; re-sending SKIP_WAITING");
|
|
79
|
+
sendSkipWaiting(registration.waiting, debug);
|
|
80
|
+
registration
|
|
81
|
+
.update()
|
|
82
|
+
.catch(() => debugLog(debug, "Service worker update retry failed (timeout path)"));
|
|
83
|
+
}
|
|
84
|
+
}, activationTimeoutMs);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
// Handle an already waiting worker at startup.
|
|
88
|
+
if (registration.waiting) {
|
|
89
|
+
handleWaiting(registration.waiting);
|
|
90
|
+
}
|
|
91
|
+
// Listen for newly installed workers becoming waiting.
|
|
92
|
+
registration.addEventListener("updatefound", () => {
|
|
93
|
+
const installing = registration.installing;
|
|
94
|
+
if (!installing)
|
|
95
|
+
return;
|
|
96
|
+
installing.addEventListener("statechange", () => {
|
|
97
|
+
if (installing.state === "installed") {
|
|
98
|
+
handleWaiting(registration.waiting);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
// Reload (or callback) once the new controller takes over.
|
|
103
|
+
navigator.serviceWorker.addEventListener("controllerchange", maybeReload, {
|
|
104
|
+
once: true,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function registerOnce(options) {
|
|
108
|
+
const { path, updateViaCache } = options;
|
|
13
109
|
if (!registrations.has(path)) {
|
|
14
110
|
const registrationPromise = navigator.serviceWorker
|
|
15
|
-
.register(path)
|
|
111
|
+
.register(path, { updateViaCache })
|
|
16
112
|
.then(async (registration) => {
|
|
17
113
|
try {
|
|
18
114
|
await registration.update();
|
|
@@ -29,18 +125,23 @@ function registerOnce(path) {
|
|
|
29
125
|
});
|
|
30
126
|
registrations.set(path, registrationPromise);
|
|
31
127
|
}
|
|
32
|
-
return registrations.get(path)
|
|
128
|
+
return registrations.get(path).then((registration) => {
|
|
129
|
+
attachUpdateHandlers(registration, options);
|
|
130
|
+
return registration;
|
|
131
|
+
});
|
|
33
132
|
}
|
|
34
133
|
/**
|
|
35
|
-
* Registers a service worker for the given path only once
|
|
36
|
-
*
|
|
134
|
+
* Registers a service worker for the given path only once, attaches an
|
|
135
|
+
* update/activation handshake (SKIP_WAITING + controllerchange reload), and
|
|
136
|
+
* caches the registration promise for subsequent calls.
|
|
37
137
|
*
|
|
38
|
-
* @param
|
|
138
|
+
* @param pathOrOptions - Service worker script path or a configuration object.
|
|
39
139
|
* @throws if service workers are not supported or registration fails.
|
|
40
140
|
*/
|
|
41
|
-
async function setupServiceWorkerOnce(
|
|
141
|
+
async function setupServiceWorkerOnce(pathOrOptions) {
|
|
42
142
|
ensureServiceWorkerSupport();
|
|
43
|
-
|
|
143
|
+
const options = normalizeOptions(pathOrOptions);
|
|
144
|
+
return registerOnce(options);
|
|
44
145
|
}
|
|
45
146
|
/**
|
|
46
147
|
* Returns an active service worker instance, optionally ensuring a specific
|
|
@@ -51,9 +152,8 @@ async function setupServiceWorkerOnce(path) {
|
|
|
51
152
|
*/
|
|
52
153
|
async function getActiveServiceWorker(path) {
|
|
53
154
|
ensureServiceWorkerSupport();
|
|
54
|
-
// Avoid mixing registrations when a specific script path is provided.
|
|
55
155
|
const registration = path
|
|
56
|
-
? await registerOnce(path)
|
|
156
|
+
? await registerOnce(normalizeOptions(path))
|
|
57
157
|
: await navigator.serviceWorker.ready;
|
|
58
158
|
let serviceWorker = registration.active ||
|
|
59
159
|
registration.waiting ||
|
|
@@ -78,5 +178,6 @@ async function getActiveServiceWorker(path) {
|
|
|
78
178
|
*/
|
|
79
179
|
const __resetServiceWorkerManager = () => {
|
|
80
180
|
registrations.clear();
|
|
181
|
+
handshakes = new WeakSet();
|
|
81
182
|
};
|
|
82
183
|
exports.__resetServiceWorkerManager = __resetServiceWorkerManager;
|
|
@@ -5,4 +5,4 @@ export { AssetInput, AssetInputs } from './assetInput.js';
|
|
|
5
5
|
export { AssetOutput, AssetOutputs } from './assetOutput.js';
|
|
6
6
|
export { Metadata, MetadataList } from './metadata.js';
|
|
7
7
|
export { AssetGroup } from './assetGroup.js';
|
|
8
|
-
export { Packet
|
|
8
|
+
export { Packet } from './packet.js';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { hex } from "@scure/base";
|
|
2
|
+
import { AssetRefType } from './types.js';
|
|
3
|
+
import { AssetGroup } from './assetGroup.js';
|
|
4
|
+
import { BufferReader, BufferWriter } from './utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Packet represents a collection of asset groups.
|
|
7
|
+
* It encodes/decodes as raw bytes only — OP_RETURN framing is handled by the Extension module.
|
|
8
|
+
*/
|
|
9
|
+
export class Packet {
|
|
10
|
+
constructor(groups) {
|
|
11
|
+
this.groups = groups;
|
|
12
|
+
}
|
|
13
|
+
static create(groups) {
|
|
14
|
+
const p = new Packet(groups);
|
|
15
|
+
p.validate();
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* fromBytes parses a Packet from raw bytes.
|
|
20
|
+
*/
|
|
21
|
+
static fromBytes(buf) {
|
|
22
|
+
return Packet.fromReader(new BufferReader(buf));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* fromString parses a Packet from a raw hex string (not an OP_RETURN script).
|
|
26
|
+
*/
|
|
27
|
+
static fromString(s) {
|
|
28
|
+
if (!s) {
|
|
29
|
+
throw new Error("missing packet data");
|
|
30
|
+
}
|
|
31
|
+
let buf;
|
|
32
|
+
try {
|
|
33
|
+
buf = hex.decode(s);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error("invalid packet format, must be hex");
|
|
37
|
+
}
|
|
38
|
+
return Packet.fromBytes(buf);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* type returns the TLV packet type tag. Implements ExtensionPacket interface.
|
|
42
|
+
*/
|
|
43
|
+
type() {
|
|
44
|
+
return Packet.PACKET_TYPE;
|
|
45
|
+
}
|
|
46
|
+
leafTxPacket(intentTxid) {
|
|
47
|
+
const leafGroups = this.groups.map((group) => group.toBatchLeafAssetGroup(intentTxid));
|
|
48
|
+
return new Packet(leafGroups);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* serialize encodes the packet as raw bytes (varint group count + group data).
|
|
52
|
+
* Does NOT include OP_RETURN, ARK magic, or TLV type/length — those are
|
|
53
|
+
* added by the Extension module.
|
|
54
|
+
*/
|
|
55
|
+
serialize() {
|
|
56
|
+
if (this.groups.length === 0) {
|
|
57
|
+
return new Uint8Array(0);
|
|
58
|
+
}
|
|
59
|
+
const writer = new BufferWriter();
|
|
60
|
+
writer.writeVarUint(this.groups.length);
|
|
61
|
+
for (const group of this.groups) {
|
|
62
|
+
group.serializeTo(writer);
|
|
63
|
+
}
|
|
64
|
+
return writer.toBytes();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* toString returns the hex-encoded raw packet bytes.
|
|
68
|
+
*/
|
|
69
|
+
toString() {
|
|
70
|
+
return hex.encode(this.serialize());
|
|
71
|
+
}
|
|
72
|
+
validate() {
|
|
73
|
+
if (this.groups.length === 0) {
|
|
74
|
+
throw new Error("missing assets");
|
|
75
|
+
}
|
|
76
|
+
const seenAssetIds = new Set();
|
|
77
|
+
for (const group of this.groups) {
|
|
78
|
+
if (group.assetId !== null) {
|
|
79
|
+
const key = group.assetId.toString();
|
|
80
|
+
if (seenAssetIds.has(key)) {
|
|
81
|
+
throw new Error(`duplicate asset group for asset ${key}`);
|
|
82
|
+
}
|
|
83
|
+
seenAssetIds.add(key);
|
|
84
|
+
}
|
|
85
|
+
if (group.controlAsset !== null &&
|
|
86
|
+
group.controlAsset.ref.type === AssetRefType.ByGroup &&
|
|
87
|
+
group.controlAsset.ref.groupIndex >= this.groups.length) {
|
|
88
|
+
throw new Error(`invalid control asset group index, ${group.controlAsset.ref.groupIndex} out of range [0, ${this.groups.length - 1}]`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
static fromReader(reader) {
|
|
93
|
+
const count = Number(reader.readVarUint());
|
|
94
|
+
const groups = [];
|
|
95
|
+
for (let i = 0; i < count; i++) {
|
|
96
|
+
groups.push(AssetGroup.fromReader(reader));
|
|
97
|
+
}
|
|
98
|
+
if (reader.remaining() > 0) {
|
|
99
|
+
throw new Error(`invalid packet length, left ${reader.remaining()} unknown bytes to read`);
|
|
100
|
+
}
|
|
101
|
+
const packet = new Packet(groups);
|
|
102
|
+
packet.validate();
|
|
103
|
+
return packet;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** PACKET_TYPE is the 1-byte TLV type tag used in the Extension envelope. */
|
|
107
|
+
Packet.PACKET_TYPE = 0;
|
|
@@ -17,6 +17,3 @@ export var AssetRefType;
|
|
|
17
17
|
export const MASK_ASSET_ID = 0x01;
|
|
18
18
|
export const MASK_CONTROL_ASSET = 0x02;
|
|
19
19
|
export const MASK_METADATA = 0x04;
|
|
20
|
-
// ARK magic bytes and marker
|
|
21
|
-
export const ARKADE_MAGIC = new Uint8Array([0x41, 0x52, 0x4b]); // "ARK"
|
|
22
|
-
export const MARKER_ASSET_PAYLOAD = 0x00;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { hex } from "@scure/base";
|
|
2
|
+
import { Script } from "@scure/btc-signer";
|
|
3
|
+
import { equalBytes } from "@scure/btc-signer/utils.js";
|
|
4
|
+
import { Packet } from './asset/packet.js';
|
|
5
|
+
import { BufferReader } from './asset/utils.js';
|
|
6
|
+
import { UnknownPacket } from './packet.js';
|
|
7
|
+
export { UnknownPacket } from './packet.js';
|
|
8
|
+
/**
|
|
9
|
+
* ArkadeMagic is the 3-byte magic prefix ("ARK") that identifies an OP_RETURN
|
|
10
|
+
* output as an ark extension blob.
|
|
11
|
+
*/
|
|
12
|
+
export const ARKADE_MAGIC = new Uint8Array([0x41, 0x52, 0x4b]); // "ARK"
|
|
13
|
+
/**
|
|
14
|
+
* ErrExtensionNotFound is thrown when no extension output is found in a transaction.
|
|
15
|
+
*/
|
|
16
|
+
export class ExtensionNotFoundError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("no extension output found in transaction");
|
|
19
|
+
this.name = "ExtensionNotFoundError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Extension is a set of typed packets encoded in an OP_RETURN output.
|
|
24
|
+
*
|
|
25
|
+
* Wire format:
|
|
26
|
+
* OP_RETURN | <push> | ARK(3B) | [type(1B) | varint_len | data]...
|
|
27
|
+
*/
|
|
28
|
+
export class Extension {
|
|
29
|
+
constructor(packets) {
|
|
30
|
+
this.packets = packets;
|
|
31
|
+
}
|
|
32
|
+
static create(packets) {
|
|
33
|
+
if (packets.length === 0) {
|
|
34
|
+
throw new Error("missing packets");
|
|
35
|
+
}
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
for (const p of packets) {
|
|
38
|
+
if (seen.has(p.type())) {
|
|
39
|
+
throw new Error(`duplicate packet type ${p.type()}`);
|
|
40
|
+
}
|
|
41
|
+
seen.add(p.type());
|
|
42
|
+
}
|
|
43
|
+
return new Extension(packets);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* isExtension returns true if the script is an OP_RETURN whose push data
|
|
47
|
+
* begins with the ARK magic bytes.
|
|
48
|
+
*/
|
|
49
|
+
static isExtension(script) {
|
|
50
|
+
try {
|
|
51
|
+
const decoded = Script.decode(script);
|
|
52
|
+
if (decoded.length < 2 || decoded[0] !== "RETURN")
|
|
53
|
+
return false;
|
|
54
|
+
const data = decoded[1];
|
|
55
|
+
if (!(data instanceof Uint8Array))
|
|
56
|
+
return false;
|
|
57
|
+
return (data.length >= ARKADE_MAGIC.length &&
|
|
58
|
+
equalBytes(data.slice(0, ARKADE_MAGIC.length), ARKADE_MAGIC));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* fromBytes parses an Extension from a raw OP_RETURN script.
|
|
66
|
+
*/
|
|
67
|
+
static fromBytes(script) {
|
|
68
|
+
if (!script || script.length === 0) {
|
|
69
|
+
throw new Error("missing OP_RETURN");
|
|
70
|
+
}
|
|
71
|
+
let decoded;
|
|
72
|
+
try {
|
|
73
|
+
decoded = Script.decode(script);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new Error("expected OP_RETURN");
|
|
77
|
+
}
|
|
78
|
+
if (decoded.length === 0 || decoded[0] !== "RETURN") {
|
|
79
|
+
throw new Error("expected OP_RETURN");
|
|
80
|
+
}
|
|
81
|
+
const dataPushes = decoded
|
|
82
|
+
.slice(1)
|
|
83
|
+
.filter((x) => x instanceof Uint8Array);
|
|
84
|
+
if (dataPushes.length === 0) {
|
|
85
|
+
throw new Error("missing magic prefix: EOF");
|
|
86
|
+
}
|
|
87
|
+
// Concatenate all data pushes (handles OP_PUSHDATA1/2/4)
|
|
88
|
+
const payload = new Uint8Array(dataPushes.reduce((acc, d) => acc + d.length, 0));
|
|
89
|
+
let offset = 0;
|
|
90
|
+
for (const d of dataPushes) {
|
|
91
|
+
payload.set(d, offset);
|
|
92
|
+
offset += d.length;
|
|
93
|
+
}
|
|
94
|
+
// Check ARK magic
|
|
95
|
+
if (payload.length < ARKADE_MAGIC.length ||
|
|
96
|
+
!equalBytes(payload.slice(0, ARKADE_MAGIC.length), ARKADE_MAGIC)) {
|
|
97
|
+
throw new Error(`expected magic prefix ${hex.encode(ARKADE_MAGIC)}, got ${hex.encode(payload.slice(0, Math.min(payload.length, ARKADE_MAGIC.length)))}`);
|
|
98
|
+
}
|
|
99
|
+
// Parse TLV records
|
|
100
|
+
const reader = new BufferReader(payload.slice(ARKADE_MAGIC.length));
|
|
101
|
+
const packets = [];
|
|
102
|
+
while (reader.remaining() > 0) {
|
|
103
|
+
const packetType = reader.readByte();
|
|
104
|
+
let data;
|
|
105
|
+
try {
|
|
106
|
+
data = reader.readVarSlice();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new Error("missing packet data");
|
|
110
|
+
}
|
|
111
|
+
packets.push(parsePacket(packetType, data));
|
|
112
|
+
}
|
|
113
|
+
if (packets.length === 0) {
|
|
114
|
+
throw new Error("missing packets");
|
|
115
|
+
}
|
|
116
|
+
// Reject duplicate packet types
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
for (const p of packets) {
|
|
119
|
+
if (seen.has(p.type())) {
|
|
120
|
+
throw new Error(`duplicate packet type ${p.type()}`);
|
|
121
|
+
}
|
|
122
|
+
seen.add(p.type());
|
|
123
|
+
}
|
|
124
|
+
return new Extension(packets);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* fromTx searches the transaction outputs for an extension blob and parses it.
|
|
128
|
+
* Throws ExtensionNotFoundError if none is found.
|
|
129
|
+
*/
|
|
130
|
+
static fromTx(tx) {
|
|
131
|
+
for (let i = 0; i < tx.outputsLength; i++) {
|
|
132
|
+
const output = tx.getOutput(i);
|
|
133
|
+
if (!output?.script)
|
|
134
|
+
continue;
|
|
135
|
+
if (Extension.isExtension(output.script)) {
|
|
136
|
+
return Extension.fromBytes(output.script);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new ExtensionNotFoundError();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* serialize encodes the extension as an OP_RETURN script.
|
|
143
|
+
*
|
|
144
|
+
* Layout: OP_RETURN | <push> | ARK | [type | varint_len | data]...
|
|
145
|
+
*/
|
|
146
|
+
serialize() {
|
|
147
|
+
// Build payload: ARK magic + TLV records
|
|
148
|
+
const parts = [ARKADE_MAGIC];
|
|
149
|
+
for (const p of this.packets) {
|
|
150
|
+
const data = p.serialize();
|
|
151
|
+
// type (1 byte)
|
|
152
|
+
const typeByte = new Uint8Array([p.type()]);
|
|
153
|
+
// varint length prefix + data
|
|
154
|
+
const lengthBuf = encodeVarUint(data.length);
|
|
155
|
+
parts.push(typeByte, lengthBuf, data);
|
|
156
|
+
}
|
|
157
|
+
const totalLen = parts.reduce((acc, p) => acc + p.length, 0);
|
|
158
|
+
const payload = new Uint8Array(totalLen);
|
|
159
|
+
let off = 0;
|
|
160
|
+
for (const p of parts) {
|
|
161
|
+
payload.set(p, off);
|
|
162
|
+
off += p.length;
|
|
163
|
+
}
|
|
164
|
+
return buildOpReturnScript(payload);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* txOut returns the extension as a zero-value OP_RETURN transaction output.
|
|
168
|
+
*/
|
|
169
|
+
txOut() {
|
|
170
|
+
return {
|
|
171
|
+
script: this.serialize(),
|
|
172
|
+
amount: 0n,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* getAssetPacket returns the embedded Packet, or null if not present.
|
|
177
|
+
*/
|
|
178
|
+
getAssetPacket() {
|
|
179
|
+
for (const p of this.packets) {
|
|
180
|
+
if (p instanceof Packet) {
|
|
181
|
+
return p;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* parsePacket dispatches to a known packet type or falls back to UnknownPacket.
|
|
189
|
+
*/
|
|
190
|
+
function parsePacket(packetType, data) {
|
|
191
|
+
if (packetType === Packet.PACKET_TYPE) {
|
|
192
|
+
return Packet.fromBytes(data);
|
|
193
|
+
}
|
|
194
|
+
return new UnknownPacket(packetType, data);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* encodeVarUint encodes a non-negative integer as a LEB128 unsigned varint.
|
|
198
|
+
*/
|
|
199
|
+
function encodeVarUint(value) {
|
|
200
|
+
const bytes = [];
|
|
201
|
+
let remaining = value;
|
|
202
|
+
do {
|
|
203
|
+
let byte = remaining & 0x7f;
|
|
204
|
+
remaining >>>= 7;
|
|
205
|
+
if (remaining > 0)
|
|
206
|
+
byte |= 0x80;
|
|
207
|
+
bytes.push(byte);
|
|
208
|
+
} while (remaining > 0);
|
|
209
|
+
return new Uint8Array(bytes);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* buildOpReturnScript builds an OP_RETURN script with an arbitrary-length data push.
|
|
213
|
+
* Manually constructed to avoid the 520-byte cap in some script builders.
|
|
214
|
+
*
|
|
215
|
+
* Opcodes: 0x6a=OP_RETURN, 0x4c=OP_PUSHDATA1, 0x4d=OP_PUSHDATA2, 0x4e=OP_PUSHDATA4
|
|
216
|
+
*/
|
|
217
|
+
function buildOpReturnScript(data) {
|
|
218
|
+
const n = data.length;
|
|
219
|
+
let script;
|
|
220
|
+
if (n <= 75) {
|
|
221
|
+
script = new Uint8Array(2 + n);
|
|
222
|
+
script[0] = 0x6a; // OP_RETURN
|
|
223
|
+
script[1] = n;
|
|
224
|
+
script.set(data, 2);
|
|
225
|
+
}
|
|
226
|
+
else if (n <= 255) {
|
|
227
|
+
script = new Uint8Array(3 + n);
|
|
228
|
+
script[0] = 0x6a;
|
|
229
|
+
script[1] = 0x4c; // OP_PUSHDATA1
|
|
230
|
+
script[2] = n;
|
|
231
|
+
script.set(data, 3);
|
|
232
|
+
}
|
|
233
|
+
else if (n <= 65535) {
|
|
234
|
+
script = new Uint8Array(4 + n);
|
|
235
|
+
script[0] = 0x6a;
|
|
236
|
+
script[1] = 0x4d; // OP_PUSHDATA2
|
|
237
|
+
new DataView(script.buffer).setUint16(2, n, true);
|
|
238
|
+
script.set(data, 4);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
script = new Uint8Array(6 + n);
|
|
242
|
+
script[0] = 0x6a;
|
|
243
|
+
script[1] = 0x4e; // OP_PUSHDATA4
|
|
244
|
+
new DataView(script.buffer).setUint32(2, n, true);
|
|
245
|
+
script.set(data, 6);
|
|
246
|
+
}
|
|
247
|
+
return script;
|
|
248
|
+
}
|