@1sat/wallet-toolbox 0.0.5 → 0.0.7

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 (38) hide show
  1. package/dist/OneSatWallet.d.ts +46 -17
  2. package/dist/OneSatWallet.js +956 -0
  3. package/dist/errors.js +11 -0
  4. package/dist/index.d.ts +0 -2
  5. package/dist/index.js +12 -93707
  6. package/dist/indexers/Bsv21Indexer.js +232 -0
  7. package/dist/indexers/CosignIndexer.js +25 -0
  8. package/dist/indexers/FundIndexer.js +64 -0
  9. package/dist/indexers/InscriptionIndexer.js +115 -0
  10. package/dist/indexers/LockIndexer.js +42 -0
  11. package/dist/indexers/MapIndexer.js +62 -0
  12. package/dist/indexers/OpNSIndexer.js +38 -0
  13. package/dist/indexers/OrdLockIndexer.js +63 -0
  14. package/dist/indexers/OriginIndexer.js +240 -0
  15. package/dist/indexers/Outpoint.js +53 -0
  16. package/dist/indexers/SigmaIndexer.js +133 -0
  17. package/dist/indexers/TransactionParser.d.ts +53 -0
  18. package/dist/indexers/index.js +13 -0
  19. package/dist/indexers/parseAddress.js +24 -0
  20. package/dist/indexers/types.js +18 -0
  21. package/dist/services/OneSatServices.d.ts +12 -4
  22. package/dist/services/OneSatServices.js +231 -0
  23. package/dist/services/client/ArcadeClient.js +107 -0
  24. package/dist/services/client/BaseClient.js +125 -0
  25. package/dist/services/client/BeefClient.js +33 -0
  26. package/dist/services/client/Bsv21Client.js +65 -0
  27. package/dist/services/client/ChaintracksClient.js +175 -0
  28. package/dist/services/client/OrdfsClient.js +122 -0
  29. package/dist/services/client/OwnerClient.js +123 -0
  30. package/dist/services/client/TxoClient.js +85 -0
  31. package/dist/services/client/index.js +8 -0
  32. package/dist/services/types.js +5 -0
  33. package/dist/signers/ReadOnlySigner.js +47 -0
  34. package/dist/sync/IndexedDbSyncQueue.js +355 -0
  35. package/dist/sync/SqliteSyncQueue.js +197 -0
  36. package/dist/sync/index.js +3 -0
  37. package/dist/sync/types.js +4 -0
  38. package/package.json +5 -5
@@ -0,0 +1,232 @@
1
+ import { BSV21 } from "@bopen-io/ts-templates";
2
+ import { HD, Hash, Utils } from "@bsv/sdk";
3
+ import { HttpError } from "../errors";
4
+ import { Indexer, } from "./types";
5
+ const FEE_XPUB = "xpub661MyMwAqRbcF221R74MPqdipLsgUevAAX4hZP2rywyEeShpbe3v2r9ciAvSGT6FB22TEmFLdUyeEDJL4ekG8s9H5WXbzDQPr6eW1zEYYy9";
6
+ const hdKey = HD.fromString(FEE_XPUB);
7
+ /**
8
+ * Bsv21Indexer identifies and validates BSV21 tokens.
9
+ * These are 1-sat outputs with application/bsv-20 inscription type.
10
+ *
11
+ * Data structure: Bsv21 with id, op, amt, dec, status, etc.
12
+ *
13
+ * Basket: 'bsv21'
14
+ * Events: id, id:status, bsv21:amt
15
+ */
16
+ export class Bsv21Indexer extends Indexer {
17
+ owners;
18
+ network;
19
+ services;
20
+ tag = "bsv21";
21
+ name = "BSV21 Tokens";
22
+ constructor(owners, network, services) {
23
+ super(owners, network);
24
+ this.owners = owners;
25
+ this.network = network;
26
+ this.services = services;
27
+ }
28
+ async parse(txo) {
29
+ const lockingScript = txo.output.lockingScript;
30
+ // Use template decode
31
+ const decoded = BSV21.decode(lockingScript);
32
+ if (!decoded)
33
+ return;
34
+ const outpoint = txo.outpoint;
35
+ const tokenData = decoded.tokenData;
36
+ // Determine token ID - for deploy ops it's this outpoint, otherwise from inscription
37
+ const tokenId = tokenData.id || outpoint.toString();
38
+ // Determine initial status:
39
+ // - deploy ops (deploy+mint, deploy+auth) are always valid (they create the token)
40
+ // - transfer/burn ops start as pending until validated in summarize()
41
+ const isDeploy = tokenData.op.startsWith("deploy");
42
+ const initialStatus = isDeploy ? "valid" : "pending";
43
+ // Create indexer data structure with basic info from inscription
44
+ const bsv21 = {
45
+ id: tokenId,
46
+ op: tokenData.op,
47
+ amt: decoded.getAmount(),
48
+ dec: decoded.getDecimals(),
49
+ sym: tokenData.sym,
50
+ icon: tokenData.icon,
51
+ status: initialStatus,
52
+ fundAddress: deriveFundAddress(outpoint.toBEBinary()),
53
+ };
54
+ // For non-deploy ops, fetch token metadata from server (cached)
55
+ // This ensures we always have sym, dec, icon for any token
56
+ if (!isDeploy) {
57
+ try {
58
+ const details = await this.services.bsv21.getTokenDetails(tokenId);
59
+ bsv21.sym = details.sym;
60
+ bsv21.icon = resolveIcon(details.icon, tokenId);
61
+ bsv21.dec = details.dec;
62
+ }
63
+ catch (e) {
64
+ // Token not found on server - could be unconfirmed or invalid
65
+ // Keep local values from inscription, status remains pending
66
+ if (!(e instanceof HttpError && e.status === 404)) {
67
+ throw e;
68
+ }
69
+ }
70
+ }
71
+ else {
72
+ // For deploy ops, resolve relative icon reference if present
73
+ bsv21.icon = resolveIcon(bsv21.icon, tokenId);
74
+ }
75
+ // Validate amount range
76
+ if (bsv21.amt <= 0n || bsv21.amt > 2n ** 64n - 1n)
77
+ return;
78
+ const tags = [];
79
+ if (txo.owner && this.owners.has(txo.owner)) {
80
+ // Use id:tokenId:status format for querying by token and status
81
+ tags.push(`id:${bsv21.id}:${bsv21.status}`);
82
+ tags.push(`amt:${bsv21.amt.toString()}`);
83
+ // Add metadata tags for efficient querying
84
+ if (bsv21.sym)
85
+ tags.push(`sym:${bsv21.sym}`);
86
+ if (bsv21.icon)
87
+ tags.push(`icon:${bsv21.icon}`);
88
+ tags.push(`dec:${bsv21.dec}`);
89
+ }
90
+ return {
91
+ data: bsv21,
92
+ tags,
93
+ basket: "bsv21",
94
+ };
95
+ }
96
+ async summarize(ctx) {
97
+ // Track token flows per token ID for validation
98
+ const tokenFlows = {};
99
+ let summaryToken;
100
+ let summaryBalance = 0;
101
+ // Process inputs from ctx.spends (already parsed)
102
+ for (const spend of ctx.spends) {
103
+ const bsv21 = spend.data.bsv21;
104
+ if (!bsv21)
105
+ continue;
106
+ const tokenData = bsv21.data;
107
+ // Initialize token tracking
108
+ if (!tokenFlows[tokenData.id]) {
109
+ tokenFlows[tokenData.id] = {
110
+ tokensIn: 0n,
111
+ tokensOut: 0n,
112
+ inputsPending: false,
113
+ };
114
+ }
115
+ const flow = tokenFlows[tokenData.id];
116
+ // Validate this input exists on the overlay
117
+ try {
118
+ await this.services.bsv21.getTokenByTxid(tokenData.id, spend.outpoint.txid);
119
+ }
120
+ catch (e) {
121
+ if (e instanceof HttpError && e.status === 404) {
122
+ // Input not on overlay yet - outputs will be pending
123
+ flow.inputsPending = true;
124
+ }
125
+ else {
126
+ throw e;
127
+ }
128
+ }
129
+ // Accumulate tokens in
130
+ flow.tokensIn += tokenData.amt;
131
+ if (!summaryToken)
132
+ summaryToken = tokenData;
133
+ // Track balance change for owned inputs
134
+ if (summaryToken &&
135
+ tokenData.id === summaryToken.id &&
136
+ spend.owner &&
137
+ this.owners.has(spend.owner)) {
138
+ summaryBalance -= Number(tokenData.amt);
139
+ }
140
+ }
141
+ // Process outputs: accumulate tokensOut and validate
142
+ for (const txo of ctx.txos) {
143
+ const bsv21 = txo.data.bsv21;
144
+ if (!bsv21 || !["transfer", "burn"].includes(bsv21.data.op))
145
+ continue;
146
+ const tokenData = bsv21.data;
147
+ const flow = tokenFlows[tokenData.id];
148
+ if (flow) {
149
+ flow.tokensOut += tokenData.amt;
150
+ }
151
+ else {
152
+ // No inputs for this token - invalid (attempting to create tokens from nothing)
153
+ tokenData.status = "invalid";
154
+ }
155
+ if (!summaryToken)
156
+ summaryToken = tokenData;
157
+ // Track balance change for owned outputs
158
+ if (summaryToken &&
159
+ tokenData.id === summaryToken.id &&
160
+ txo.owner &&
161
+ this.owners.has(txo.owner)) {
162
+ summaryBalance += Number(tokenData.amt);
163
+ }
164
+ }
165
+ // Finalize validation and apply status to outputs
166
+ for (const txo of ctx.txos) {
167
+ const bsv21 = txo.data.bsv21;
168
+ if (!bsv21 || !["transfer", "burn"].includes(bsv21.data.op))
169
+ continue;
170
+ const tokenData = bsv21.data;
171
+ if (tokenData.status === "invalid")
172
+ continue; // Already marked invalid
173
+ const flow = tokenFlows[tokenData.id];
174
+ if (!flow)
175
+ continue;
176
+ // Determine final status
177
+ if (flow.inputsPending) {
178
+ tokenData.status = "pending";
179
+ }
180
+ else if (flow.tokensIn >= flow.tokensOut) {
181
+ tokenData.status = "valid";
182
+ }
183
+ else {
184
+ tokenData.status = "invalid";
185
+ }
186
+ }
187
+ if (summaryToken?.sym) {
188
+ return {
189
+ id: summaryToken.sym,
190
+ amount: summaryBalance / 10 ** (summaryToken.dec || 0),
191
+ icon: summaryToken.icon,
192
+ };
193
+ }
194
+ }
195
+ serialize(bsv21) {
196
+ return JSON.stringify({
197
+ ...bsv21,
198
+ amt: bsv21.amt.toString(10),
199
+ });
200
+ }
201
+ deserialize(str) {
202
+ const obj = JSON.parse(str);
203
+ return {
204
+ ...obj,
205
+ amt: BigInt(obj.amt),
206
+ };
207
+ }
208
+ }
209
+ /**
210
+ * Resolve a relative icon reference to an absolute outpoint.
211
+ * If icon starts with "_", it's a relative reference to another output
212
+ * in the same transaction as the token deploy. We resolve it by
213
+ * prepending the token ID's txid.
214
+ *
215
+ * Example: tokenId = "abc123...def_0", icon = "_1" -> "abc123...def_1"
216
+ */
217
+ function resolveIcon(icon, tokenId) {
218
+ if (!icon || !icon.startsWith("_"))
219
+ return icon;
220
+ // Token ID format is "txid_vout", extract the txid
221
+ const txid = tokenId.substring(0, 64);
222
+ // Icon format is "_vout", combine with txid
223
+ return `${txid}${icon}`;
224
+ }
225
+ export function deriveFundAddress(idOrOutpoint) {
226
+ const hash = Hash.sha256(idOrOutpoint);
227
+ const reader = new Utils.Reader(hash);
228
+ let path = `m/21/${reader.readUInt32BE() >> 1}`;
229
+ reader.pos = 24;
230
+ path += `/${reader.readUInt32BE() >> 1}`;
231
+ return hdKey.derive(path).pubKey.toAddress();
232
+ }
@@ -0,0 +1,25 @@
1
+ import { Cosign } from "@bopen-io/ts-templates";
2
+ import { Indexer } from "./types";
3
+ export class CosignIndexer extends Indexer {
4
+ owners;
5
+ network;
6
+ tag = "cosign";
7
+ name = "Cosign";
8
+ constructor(owners = new Set(), network = "mainnet") {
9
+ super(owners, network);
10
+ this.owners = owners;
11
+ this.network = network;
12
+ }
13
+ async parse(txo) {
14
+ const lockingScript = txo.output.lockingScript;
15
+ // Use template decode
16
+ const decoded = Cosign.decode(lockingScript, this.network === "mainnet");
17
+ if (!decoded)
18
+ return;
19
+ return {
20
+ data: decoded,
21
+ tags: [],
22
+ owner: decoded.address,
23
+ };
24
+ }
25
+ }
@@ -0,0 +1,64 @@
1
+ import { parseAddress } from "./parseAddress";
2
+ import { Indexer, } from "./types";
3
+ /**
4
+ * FundIndexer identifies P2PKH outputs to owned addresses.
5
+ * These are standard "funding" UTXOs that can be spent normally.
6
+ *
7
+ * Data structure: string (address)
8
+ *
9
+ * Basket: 'fund'
10
+ * Tags: None
11
+ */
12
+ export class FundIndexer extends Indexer {
13
+ owners;
14
+ network;
15
+ tag = "fund";
16
+ name = "Funds";
17
+ constructor(owners = new Set(), network = "mainnet") {
18
+ super(owners, network);
19
+ this.owners = owners;
20
+ this.network = network;
21
+ }
22
+ async parse(txo) {
23
+ const script = txo.output.lockingScript;
24
+ const satoshis = BigInt(txo.output.satoshis || 0);
25
+ const address = parseAddress(script, 0, this.network);
26
+ if (satoshis < 2n)
27
+ return;
28
+ return {
29
+ data: address,
30
+ tags: [],
31
+ owner: address,
32
+ basket: "fund",
33
+ };
34
+ }
35
+ async summarize(ctx) {
36
+ let satsOut = 0n;
37
+ let satsIn = 0n;
38
+ // Calculate satoshis spent from our addresses (inputs)
39
+ for (const input of ctx.tx.inputs) {
40
+ if (!input.sourceTransaction) {
41
+ // If we don't have source transaction data, we can't determine balance change
42
+ return;
43
+ }
44
+ const sourceOutput = input.sourceTransaction.outputs[input.sourceOutputIndex];
45
+ const address = parseAddress(sourceOutput.lockingScript, 0, this.network);
46
+ if (this.owners.has(address)) {
47
+ satsOut += BigInt(sourceOutput.satoshis || 0);
48
+ }
49
+ }
50
+ // Calculate satoshis received to our addresses (outputs)
51
+ satsIn = ctx.txos.reduce((acc, txo) => {
52
+ if (!txo.data[this.tag])
53
+ return acc;
54
+ const satoshis = BigInt(txo.output.satoshis || 0);
55
+ return acc + (txo.owner && this.owners.has(txo.owner) ? satoshis : 0n);
56
+ }, 0n);
57
+ const balance = Number(satsIn - satsOut);
58
+ if (balance) {
59
+ return {
60
+ amount: balance,
61
+ };
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,115 @@
1
+ import { Inscription as InscriptionTemplate } from "@bopen-io/ts-templates";
2
+ import { OP, Script, Utils } from "@bsv/sdk";
3
+ import { MapIndexer } from "./MapIndexer";
4
+ import { parseAddress } from "./parseAddress";
5
+ import { Indexer, } from "./types";
6
+ /**
7
+ * InscriptionIndexer identifies and parses ordinal inscriptions.
8
+ * These are outputs with exactly 1 satoshi containing OP_FALSE OP_IF "ord" envelope.
9
+ *
10
+ * Data structure: Inscription with file, fields, and optional parent
11
+ *
12
+ * Basket: None (no basket assignment - this is preliminary data for other indexers)
13
+ * Events: address for owned outputs
14
+ */
15
+ export class InscriptionIndexer extends Indexer {
16
+ owners;
17
+ network;
18
+ tag = "insc";
19
+ name = "Inscriptions";
20
+ constructor(owners = new Set(), network = "mainnet") {
21
+ super(owners, network);
22
+ this.owners = owners;
23
+ this.network = network;
24
+ }
25
+ async parse(txo) {
26
+ const satoshis = BigInt(txo.output.satoshis || 0);
27
+ if (satoshis !== 1n)
28
+ return;
29
+ const script = txo.output.lockingScript;
30
+ // Use template decode
31
+ const decoded = InscriptionTemplate.decode(script);
32
+ if (!decoded)
33
+ return;
34
+ // Extract owner from script prefix or suffix
35
+ let owner = parseAddress(script, 0, this.network);
36
+ if (!owner && decoded.scriptSuffix) {
37
+ // Try to find owner in suffix (after OP_ENDIF)
38
+ const suffixScript = Script.fromBinary(Array.from(decoded.scriptSuffix));
39
+ owner = parseAddress(suffixScript, 0, this.network);
40
+ // Also check for OP_CODESEPARATOR pattern
41
+ if (!owner && suffixScript.chunks[0]?.op === OP.OP_CODESEPARATOR) {
42
+ owner = parseAddress(suffixScript, 1, this.network);
43
+ }
44
+ }
45
+ // Handle MAP field if present (special case)
46
+ // Note: This writes to txo.data.map directly as a side effect
47
+ if (decoded.fields?.has("MAP")) {
48
+ const mapData = decoded.fields.get("MAP");
49
+ if (mapData) {
50
+ const map = MapIndexer.parseMap(Script.fromBinary(Array.from(mapData)), 0);
51
+ if (map) {
52
+ txo.data.map = { data: map, tags: [] };
53
+ }
54
+ }
55
+ }
56
+ // Convert to wallet-toolbox format
57
+ const insc = {
58
+ file: {
59
+ hash: Utils.toBase64(Array.from(decoded.file.hash)),
60
+ size: decoded.file.size,
61
+ type: decoded.file.type,
62
+ content: Array.from(decoded.file.content),
63
+ },
64
+ fields: {},
65
+ };
66
+ // Extract text content if it's a text-based inscription and small enough
67
+ let content;
68
+ const contentType = decoded.file.type.toLowerCase();
69
+ const isTextContent = contentType.startsWith("text/") || contentType === "application/json";
70
+ if (isTextContent && decoded.file.size <= 1000) {
71
+ try {
72
+ content = new TextDecoder().decode(decoded.file.content);
73
+ }
74
+ catch {
75
+ // Ignore decoding errors
76
+ }
77
+ }
78
+ // Convert parent outpoint to string format
79
+ if (decoded.parent) {
80
+ try {
81
+ const reader = new Utils.Reader(Array.from(decoded.parent));
82
+ const txid = Utils.toHex(reader.read(32).reverse());
83
+ const vout = reader.readInt32LE();
84
+ insc.parent = `${txid}_${vout}`;
85
+ }
86
+ catch {
87
+ // Ignore parsing errors
88
+ }
89
+ }
90
+ // Convert fields to base64 strings
91
+ if (decoded.fields && insc.fields) {
92
+ for (const [key, value] of decoded.fields) {
93
+ if (key !== "MAP") {
94
+ insc.fields[key] = Buffer.from(value).toString("base64");
95
+ }
96
+ }
97
+ }
98
+ return {
99
+ data: insc,
100
+ tags: [],
101
+ owner,
102
+ content,
103
+ };
104
+ }
105
+ async summarize(ctx) {
106
+ // Clear file content before saving - content is loaded locally but shouldn't be persisted
107
+ for (const txo of ctx.txos) {
108
+ const insc = txo.data[this.tag]?.data;
109
+ if (insc?.file) {
110
+ insc.file.content = [];
111
+ }
112
+ }
113
+ return undefined;
114
+ }
115
+ }
@@ -0,0 +1,42 @@
1
+ import { Lock } from "@bopen-io/ts-templates";
2
+ import { Indexer, } from "./types";
3
+ export class LockIndexer extends Indexer {
4
+ tag = "lock";
5
+ name = "Locks";
6
+ async parse(txo) {
7
+ const lockingScript = txo.output.lockingScript;
8
+ const decoded = Lock.decode(lockingScript, this.network === "mainnet");
9
+ if (!decoded)
10
+ return;
11
+ const tags = [];
12
+ if (this.owners.has(decoded.address)) {
13
+ tags.push(`lock:until:${decoded.until}`);
14
+ }
15
+ return {
16
+ data: { until: decoded.until },
17
+ tags,
18
+ owner: decoded.address,
19
+ basket: "lock",
20
+ };
21
+ }
22
+ async summarize(ctx) {
23
+ let locksOut = 0n;
24
+ for (const spend of ctx.spends) {
25
+ if (spend.data[this.tag]) {
26
+ const satoshis = BigInt(spend.output.satoshis || 0);
27
+ locksOut += spend.owner && this.owners.has(spend.owner) ? satoshis : 0n;
28
+ }
29
+ }
30
+ let locksIn = 0n;
31
+ for (const txo of ctx.txos) {
32
+ if (txo.data[this.tag]) {
33
+ const satoshis = BigInt(txo.output.satoshis || 0);
34
+ locksIn += txo.owner && this.owners.has(txo.owner) ? satoshis : 0n;
35
+ }
36
+ }
37
+ const balance = Number(locksIn - locksOut);
38
+ if (balance) {
39
+ return { amount: balance };
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,62 @@
1
+ import { MAP_PREFIX } from "@bopen-io/ts-templates";
2
+ import { OP, Script, Utils } from "@bsv/sdk";
3
+ import { Indexer } from "./types";
4
+ export class MapIndexer extends Indexer {
5
+ owners;
6
+ network;
7
+ tag = "map";
8
+ name = "MAP";
9
+ constructor(owners = new Set(), network = "mainnet") {
10
+ super(owners, network);
11
+ this.owners = owners;
12
+ this.network = network;
13
+ }
14
+ async parse(txo) {
15
+ const script = txo.output.lockingScript;
16
+ const retPos = script.chunks.findIndex((chunk) => chunk.op === OP.OP_RETURN);
17
+ if (retPos < 0 || !script.chunks[retPos]?.data?.length) {
18
+ return;
19
+ }
20
+ let chunks = Script.fromBinary(script.chunks[retPos].data).chunks;
21
+ while (chunks.length) {
22
+ if (Utils.toUTF8(chunks[0]?.data || []) === MAP_PREFIX) {
23
+ const map = MapIndexer.parseMap(new Script(chunks), 1);
24
+ const name = (map?.name ?? map?.subTypeData?.name ?? 'Unknown');
25
+ const tags = name ? [`name:${name}`] : [];
26
+ return map ? { data: map, tags } : undefined;
27
+ }
28
+ const pipePos = chunks.findIndex((chunk) => chunk.data?.length === 1 && chunk.data[0] !== 0x7c);
29
+ if (pipePos > -1) {
30
+ chunks = chunks.slice(pipePos + 1);
31
+ }
32
+ else
33
+ break;
34
+ }
35
+ }
36
+ static parseMap(script, mapPos) {
37
+ if (Utils.toUTF8(script.chunks[mapPos]?.data || []) !== "SET") {
38
+ return;
39
+ }
40
+ const map = {};
41
+ for (let i = mapPos + 1; i < script.chunks.length; i += 2) {
42
+ const chunk = script.chunks[i];
43
+ if (chunk.op === OP.OP_RETURN ||
44
+ (chunk.data?.length === 1 && chunk.data[0] !== 0x7c)) {
45
+ break;
46
+ }
47
+ const key = Utils.toUTF8(chunk.data || []);
48
+ const value = Utils.toUTF8(script.chunks[i + 1]?.data || []);
49
+ if (key === "subTypeData") {
50
+ try {
51
+ map[key] = JSON.parse(value);
52
+ continue;
53
+ }
54
+ catch {
55
+ // If JSON parsing fails, fall through to store as string
56
+ }
57
+ }
58
+ map[key] = value;
59
+ }
60
+ return map;
61
+ }
62
+ }
@@ -0,0 +1,38 @@
1
+ import { Utils } from "@bsv/sdk";
2
+ import { Indexer } from "./types";
3
+ export class OpNSIndexer extends Indexer {
4
+ owners;
5
+ network;
6
+ tag = "opns";
7
+ name = "OpNS";
8
+ constructor(owners = new Set(), network = "mainnet") {
9
+ super(owners, network);
10
+ this.owners = owners;
11
+ this.network = network;
12
+ }
13
+ async parse(txo) {
14
+ const insc = txo.data.insc?.data;
15
+ if (insc?.file?.type !== "application/op-ns")
16
+ return;
17
+ const tags = [];
18
+ // Extract name from inscription content
19
+ if (insc.file?.content && txo.owner && this.owners.has(txo.owner)) {
20
+ try {
21
+ const content = Utils.toUTF8(insc.file.content);
22
+ const data = JSON.parse(content);
23
+ if (data.name) {
24
+ tags.push(`name:${data.name}`);
25
+ }
26
+ }
27
+ catch {
28
+ // Invalid JSON or missing name field
29
+ }
30
+ }
31
+ // TODO: Add validation against OpNS server (infrastructure not ready yet)
32
+ return {
33
+ data: insc,
34
+ tags,
35
+ basket: "opns",
36
+ };
37
+ }
38
+ }
@@ -0,0 +1,63 @@
1
+ import { OrdLock } from "@bopen-io/ts-templates";
2
+ import { Indexer, } from "./types";
3
+ export class Listing {
4
+ payout;
5
+ price;
6
+ constructor(payout = [], price = 0n) {
7
+ this.payout = payout;
8
+ this.price = price;
9
+ }
10
+ }
11
+ export class OrdLockIndexer extends Indexer {
12
+ owners;
13
+ network;
14
+ tag = "list";
15
+ name = "Listings";
16
+ constructor(owners = new Set(), network = "mainnet") {
17
+ super(owners, network);
18
+ this.owners = owners;
19
+ this.network = network;
20
+ }
21
+ async parse(txo) {
22
+ const lockingScript = txo.output.lockingScript;
23
+ const decoded = OrdLock.decode(lockingScript, this.network === "mainnet");
24
+ if (!decoded)
25
+ return;
26
+ const listing = new Listing(decoded.payout, decoded.price);
27
+ return {
28
+ data: listing,
29
+ tags: [`list:${listing.price}`],
30
+ owner: decoded.seller,
31
+ };
32
+ }
33
+ async summarize(ctx) {
34
+ // Check if any input was spending a listing
35
+ for (const [vin, spend] of ctx.spends.entries()) {
36
+ if (spend.data[this.tag]) {
37
+ const unlockingScript = ctx.tx.inputs[vin].unlockingScript;
38
+ if (unlockingScript && OrdLock.isPurchase(unlockingScript)) {
39
+ // Purchased via ordlock contract
40
+ return { amount: 1 };
41
+ }
42
+ // Cancelled/reclaimed by owner
43
+ return { amount: 0 };
44
+ }
45
+ }
46
+ // Check if any output is creating a listing
47
+ for (const txo of ctx.txos) {
48
+ if (txo.data[this.tag]) {
49
+ return { amount: -1 };
50
+ }
51
+ }
52
+ }
53
+ serialize(listing) {
54
+ return JSON.stringify({
55
+ payout: listing.payout,
56
+ price: listing.price.toString(10),
57
+ });
58
+ }
59
+ deserialize(str) {
60
+ const obj = JSON.parse(str);
61
+ return new Listing(obj.payout, BigInt(obj.price));
62
+ }
63
+ }