@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.
Files changed (63) hide show
  1. package/dist/cjs/{asset → extension/asset}/index.js +1 -2
  2. package/dist/cjs/extension/asset/packet.js +111 -0
  3. package/dist/cjs/{asset → extension/asset}/types.js +1 -4
  4. package/dist/cjs/extension/index.js +254 -0
  5. package/dist/cjs/extension/packet.js +20 -0
  6. package/dist/cjs/index.js +1 -1
  7. package/dist/cjs/providers/ark.js +52 -37
  8. package/dist/cjs/providers/indexer.js +1 -1
  9. package/dist/cjs/providers/utils.js +39 -29
  10. package/dist/cjs/wallet/asset-manager.js +9 -17
  11. package/dist/cjs/wallet/asset.js +1 -1
  12. package/dist/cjs/wallet/validation.js +6 -2
  13. package/dist/cjs/wallet/wallet.js +5 -4
  14. package/dist/cjs/worker/browser/service-worker-manager.js +111 -10
  15. package/dist/esm/{asset → extension/asset}/index.js +1 -1
  16. package/dist/esm/extension/asset/packet.js +107 -0
  17. package/dist/esm/{asset → extension/asset}/types.js +0 -3
  18. package/dist/esm/extension/index.js +248 -0
  19. package/dist/esm/extension/packet.js +16 -0
  20. package/dist/esm/index.js +1 -1
  21. package/dist/esm/providers/ark.js +52 -37
  22. package/dist/esm/providers/indexer.js +1 -1
  23. package/dist/esm/providers/utils.js +39 -29
  24. package/dist/esm/wallet/asset-manager.js +9 -17
  25. package/dist/esm/wallet/asset.js +1 -1
  26. package/dist/esm/wallet/validation.js +6 -2
  27. package/dist/esm/wallet/wallet.js +5 -4
  28. package/dist/esm/worker/browser/service-worker-manager.js +111 -10
  29. package/dist/types/{asset → extension/asset}/index.d.ts +1 -1
  30. package/dist/types/extension/asset/packet.d.ts +38 -0
  31. package/dist/types/{asset → extension/asset}/types.d.ts +0 -2
  32. package/dist/types/extension/index.d.ts +56 -0
  33. package/dist/types/extension/packet.d.ts +21 -0
  34. package/dist/types/index.d.ts +1 -1
  35. package/dist/types/providers/utils.d.ts +6 -0
  36. package/dist/types/wallet/asset-manager.d.ts +4 -13
  37. package/dist/types/wallet/asset.d.ts +1 -1
  38. package/dist/types/worker/browser/service-worker-manager.d.ts +15 -4
  39. package/package.json +1 -1
  40. package/dist/cjs/asset/packet.js +0 -164
  41. package/dist/esm/asset/packet.js +0 -159
  42. package/dist/types/asset/packet.d.ts +0 -27
  43. /package/dist/cjs/{asset → extension/asset}/assetGroup.js +0 -0
  44. /package/dist/cjs/{asset → extension/asset}/assetId.js +0 -0
  45. /package/dist/cjs/{asset → extension/asset}/assetInput.js +0 -0
  46. /package/dist/cjs/{asset → extension/asset}/assetOutput.js +0 -0
  47. /package/dist/cjs/{asset → extension/asset}/assetRef.js +0 -0
  48. /package/dist/cjs/{asset → extension/asset}/metadata.js +0 -0
  49. /package/dist/cjs/{asset → extension/asset}/utils.js +0 -0
  50. /package/dist/esm/{asset → extension/asset}/assetGroup.js +0 -0
  51. /package/dist/esm/{asset → extension/asset}/assetId.js +0 -0
  52. /package/dist/esm/{asset → extension/asset}/assetInput.js +0 -0
  53. /package/dist/esm/{asset → extension/asset}/assetOutput.js +0 -0
  54. /package/dist/esm/{asset → extension/asset}/assetRef.js +0 -0
  55. /package/dist/esm/{asset → extension/asset}/metadata.js +0 -0
  56. /package/dist/esm/{asset → extension/asset}/utils.js +0 -0
  57. /package/dist/types/{asset → extension/asset}/assetGroup.d.ts +0 -0
  58. /package/dist/types/{asset → extension/asset}/assetId.d.ts +0 -0
  59. /package/dist/types/{asset → extension/asset}/assetInput.d.ts +0 -0
  60. /package/dist/types/{asset → extension/asset}/assetOutput.d.ts +0 -0
  61. /package/dist/types/{asset → extension/asset}/assetRef.d.ts +0 -0
  62. /package/dist/types/{asset → extension/asset}/metadata.d.ts +0 -0
  63. /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.controlAsset - Optional control asset (for reissuable assets)
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.issueAsset({ amount: 1000 });
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.issueAsset({
39
+ * const result = await wallet.assetManager.issue({
47
40
  * amount: 1000,
48
- * controlAsset: 'controlAssetId'
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;
@@ -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 asset_1 = require("../asset");
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 assetPacket = asset_1.Packet.fromTx(leafTx);
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 registerOnce(path) {
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 and caches the
36
- * registration promise for subsequent calls.
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 path - Service worker script path to register.
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(path) {
141
+ async function setupServiceWorkerOnce(pathOrOptions) {
42
142
  ensureServiceWorkerSupport();
43
- return registerOnce(path);
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, AssetPacketNotFoundError } from './packet.js';
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
+ }