@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.
- package/dist/OneSatWallet.d.ts +46 -17
- package/dist/OneSatWallet.js +956 -0
- package/dist/errors.js +11 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +12 -93707
- package/dist/indexers/Bsv21Indexer.js +232 -0
- package/dist/indexers/CosignIndexer.js +25 -0
- package/dist/indexers/FundIndexer.js +64 -0
- package/dist/indexers/InscriptionIndexer.js +115 -0
- package/dist/indexers/LockIndexer.js +42 -0
- package/dist/indexers/MapIndexer.js +62 -0
- package/dist/indexers/OpNSIndexer.js +38 -0
- package/dist/indexers/OrdLockIndexer.js +63 -0
- package/dist/indexers/OriginIndexer.js +240 -0
- package/dist/indexers/Outpoint.js +53 -0
- package/dist/indexers/SigmaIndexer.js +133 -0
- package/dist/indexers/TransactionParser.d.ts +53 -0
- package/dist/indexers/index.js +13 -0
- package/dist/indexers/parseAddress.js +24 -0
- package/dist/indexers/types.js +18 -0
- package/dist/services/OneSatServices.d.ts +12 -4
- package/dist/services/OneSatServices.js +231 -0
- package/dist/services/client/ArcadeClient.js +107 -0
- package/dist/services/client/BaseClient.js +125 -0
- package/dist/services/client/BeefClient.js +33 -0
- package/dist/services/client/Bsv21Client.js +65 -0
- package/dist/services/client/ChaintracksClient.js +175 -0
- package/dist/services/client/OrdfsClient.js +122 -0
- package/dist/services/client/OwnerClient.js +123 -0
- package/dist/services/client/TxoClient.js +85 -0
- package/dist/services/client/index.js +8 -0
- package/dist/services/types.js +5 -0
- package/dist/signers/ReadOnlySigner.js +47 -0
- package/dist/sync/IndexedDbSyncQueue.js +355 -0
- package/dist/sync/SqliteSyncQueue.js +197 -0
- package/dist/sync/index.js +3 -0
- package/dist/sync/types.js +4 -0
- 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
|
+
}
|