@hardkas/toolkit 0.11.1-alpha → 0.11.2-alpha

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.
@@ -1,53 +1,62 @@
1
- import { DomainStoreJson } from '@hardkas/query-store';
2
-
3
- export interface InvoiceRecord {
4
- id: string;
5
- merchantId: string;
6
- amount: bigint;
7
- currency: string;
8
- status: 'pending' | 'paid' | 'cancelled';
9
- uri: string;
10
- createdAt: string;
11
- updatedAt: string;
12
- }
13
-
14
- export class InvoiceStoreJson {
15
- private store: DomainStoreJson<InvoiceRecord>;
16
-
17
- constructor(options: { filePath: string }) {
18
- this.store = new DomainStoreJson<InvoiceRecord>(options);
19
- }
20
-
21
- public save(invoice: InvoiceRecord): void {
22
- invoice.updatedAt = new Date().toISOString();
23
- this.store.save(invoice.id, invoice);
24
- }
25
-
26
- public get(id: string): InvoiceRecord | undefined {
27
- return this.store.get(id);
28
- }
29
-
30
- public list(): InvoiceRecord[] {
31
- return this.store.list();
32
- }
33
-
34
- public listByMerchant(merchantId: string): InvoiceRecord[] {
35
- return this.store.list().filter(i => i.merchantId === merchantId);
36
- }
37
-
38
- public listPaid(): InvoiceRecord[] {
39
- return this.store.list().filter(i => i.status === 'paid');
40
- }
41
-
42
- public stats(merchantId?: string) {
43
- let invoices = this.store.list();
44
- if (merchantId) {
45
- invoices = invoices.filter(i => i.merchantId === merchantId);
46
- }
47
-
48
- return {
49
- totalInvoices: invoices.length,
50
- paidInvoices: invoices.filter(i => i.status === 'paid').length
51
- };
52
- }
53
- }
1
+ import { DomainStoreJson } from '@hardkas/query-store';
2
+
3
+ export interface InvoiceRecord {
4
+ id: string;
5
+ merchantId: string;
6
+ amount: bigint;
7
+ currency: string;
8
+ status: 'pending' | 'paid' | 'cancelled';
9
+ uri: string;
10
+ createdAt: string;
11
+ updatedAt: string;
12
+ }
13
+
14
+ export interface InvoiceStore {
15
+ save(invoice: InvoiceRecord): Promise<void> | void;
16
+ get(id: string): Promise<InvoiceRecord | undefined> | InvoiceRecord | undefined;
17
+ list(): Promise<InvoiceRecord[]> | InvoiceRecord[];
18
+ listByMerchant(merchantId: string): Promise<InvoiceRecord[]> | InvoiceRecord[];
19
+ listPaid(): Promise<InvoiceRecord[]> | InvoiceRecord[];
20
+ stats(merchantId?: string): Promise<{ totalInvoices: number; paidInvoices: number }> | { totalInvoices: number; paidInvoices: number };
21
+ }
22
+
23
+ export class InvoiceStoreJson implements InvoiceStore {
24
+ private store: DomainStoreJson<InvoiceRecord>;
25
+
26
+ constructor(options: { filePath: string }) {
27
+ this.store = new DomainStoreJson<InvoiceRecord>(options);
28
+ }
29
+
30
+ public save(invoice: InvoiceRecord): void {
31
+ invoice.updatedAt = new Date().toISOString();
32
+ this.store.save(invoice.id, invoice);
33
+ }
34
+
35
+ public get(id: string): InvoiceRecord | undefined {
36
+ return this.store.get(id);
37
+ }
38
+
39
+ public list(): InvoiceRecord[] {
40
+ return this.store.list();
41
+ }
42
+
43
+ public listByMerchant(merchantId: string): InvoiceRecord[] {
44
+ return this.store.list().filter(i => i.merchantId === merchantId);
45
+ }
46
+
47
+ public listPaid(): InvoiceRecord[] {
48
+ return this.store.list().filter(i => i.status === 'paid');
49
+ }
50
+
51
+ public stats(merchantId?: string) {
52
+ let invoices = this.store.list();
53
+ if (merchantId) {
54
+ invoices = invoices.filter(i => i.merchantId === merchantId);
55
+ }
56
+
57
+ return {
58
+ totalInvoices: invoices.length,
59
+ paidInvoices: invoices.filter(i => i.status === 'paid').length
60
+ };
61
+ }
62
+ }
package/src/wallet.ts CHANGED
@@ -1,93 +1,158 @@
1
- import { WalletManagerImpl, WalletStateStoreJson, AddressManager } from '@hardkas/accounts';
2
- import { WalletQuery } from '@hardkas/query';
3
- import { selectCoins, estimateFee, buildPaymentPlan } from '@hardkas/tx-builder';
4
- import { WalletUtxoApi } from './utxos.js';
5
- import { UtxoControlStore } from './stores/utxo-control-store.js';
6
-
7
- export interface WalletToolkitOptions {
8
- storePath?: string;
9
- network?: "simnet" | "testnet" | "mainnet";
10
- }
11
-
12
- export class WalletToolkit {
13
- private walletManager: WalletManagerImpl;
14
- private addressManager: typeof AddressManager;
15
- private walletQuery: WalletQuery;
16
-
17
- private _utxosApi: WalletUtxoApi;
18
-
19
- private constructor(
20
- public readonly name: string,
21
- private readonly store: WalletStateStoreJson,
22
- private readonly options: WalletToolkitOptions
23
- ) {
24
- this.walletManager = new WalletManagerImpl();
25
- this.addressManager = AddressManager;
26
- this.walletQuery = new WalletQuery({} as any);
27
-
28
- const utxoControlPath = options.storePath
29
- ? options.storePath.replace('.json', '-utxo-control.json')
30
- : 'default-utxo-control.json';
31
- const utxoStore = new UtxoControlStore(utxoControlPath);
32
-
33
- this._utxosApi = new WalletUtxoApi(
34
- async () => (await this.walletQuery.getUtxos([await this.address()])) as any,
35
- async () => this.address(),
36
- async (inputs: number, outputs: number) => BigInt(inputs * 1000 + outputs * 1000), // Dummy fee estimation
37
- utxoStore
38
- );
39
- }
40
-
41
- public static open(name: string, options: WalletToolkitOptions = {}): WalletToolkit {
42
- const store = new WalletStateStoreJson({ filePath: options.storePath || 'default.json' });
43
- return new WalletToolkit(name, store, options);
44
- }
45
-
46
- public async create(): Promise<void> {
47
- this.walletManager.create({ walletId: this.name });
48
- }
49
-
50
- public async address(): Promise<string> {
51
- // High level facade: gets receive address
52
- const seedRef = this.walletManager.getSeedRef(this.name);
53
- return this.addressManager.derive({
54
- seedRef,
55
- accountIndex: 0,
56
- chain: 'receive',
57
- addressIndex: 0,
58
- network: this.options.network || 'simnet'
59
- }).address;
60
- }
61
-
62
- public async balance(): Promise<bigint> {
63
- const addr = await this.address();
64
- return (await this.walletQuery.getBalance([addr])) as any;
65
- }
66
-
67
- public get utxos(): WalletUtxoApi {
68
- return this._utxosApi;
69
- }
70
-
71
- public async history(): Promise<any[]> {
72
- const addr = await this.address();
73
- return (await this.walletQuery.getHistory({ addresses: [addr] })) as any;
74
- }
75
-
76
- public async estimateFee(opts: { to: string; amount: bigint }): Promise<any> {
77
- const u = await this.utxos.list();
78
- // Since we are mocking the facade, just return a fake fee
79
- return {
80
- selectedUtxos: [],
81
- fee: 1000n,
82
- totalOut: BigInt(opts.amount) + 1000n
83
- };
84
- }
85
-
86
- public async planSend(opts: { to: string; amount: bigint }): Promise<any> {
87
- return {};
88
- }
89
-
90
- public async sendSimulated(opts: { to: string; amount: bigint }): Promise<string> {
91
- return `simulated_tx_${Date.now()}`;
92
- }
93
- }
1
+ import { WalletManagerImpl, WalletStateStoreJson, AddressManager } from '@hardkas/accounts';
2
+ import { WalletQuery, WalletQueryProvider } from '@hardkas/query';
3
+ import { selectCoins, estimateFee, buildPaymentPlan } from '@hardkas/tx-builder';
4
+ import { WalletUtxoApi } from './utxos.js';
5
+ import { UtxoControlStore } from './stores/utxo-control-store.js';
6
+ import { logger, metrics, tracer } from '@hardkas/observability';
7
+
8
+ metrics.register({
9
+ name: "wallet_tx_generated_total",
10
+ help: "Total tx generated by wallet",
11
+ type: "counter"
12
+ });
13
+ metrics.register({
14
+ name: "wallet_tx_submitted_total",
15
+ help: "Total tx submitted by wallet",
16
+ type: "counter"
17
+ });
18
+ metrics.register({
19
+ name: "wallet_tx_failed_total",
20
+ help: "Total tx failed by wallet",
21
+ type: "counter"
22
+ });
23
+
24
+ export interface WalletToolkitOptions {
25
+ storePath?: string;
26
+ network?: "simnet" | "testnet" | "mainnet";
27
+ provider?: WalletQueryProvider;
28
+ }
29
+
30
+ export class WalletToolkit {
31
+ private walletManager: WalletManagerImpl;
32
+ private addressManager: typeof AddressManager;
33
+ private walletQuery: WalletQuery;
34
+
35
+ private _utxosApi: WalletUtxoApi;
36
+
37
+ private constructor(
38
+ public readonly name: string,
39
+ private readonly store: WalletStateStoreJson,
40
+ private readonly options: WalletToolkitOptions
41
+ ) {
42
+ this.walletManager = new WalletManagerImpl();
43
+ this.addressManager = AddressManager;
44
+
45
+ const dummyProvider: WalletQueryProvider = {
46
+ source: "dummy",
47
+ async getBalances() { return {}; },
48
+ async getUtxos() { return {}; },
49
+ async getHistory() { return { items: [] }; }
50
+ };
51
+
52
+ this.walletQuery = new WalletQuery({ provider: options.provider || dummyProvider, network: options.network || "simnet" });
53
+
54
+ const utxoControlPath = options.storePath
55
+ ? options.storePath.replace('.json', '-utxo-control.json')
56
+ : 'default-utxo-control.json';
57
+ const utxoStore = new UtxoControlStore(utxoControlPath);
58
+
59
+ this._utxosApi = new WalletUtxoApi(
60
+ async () => {
61
+ const addr = await this.address();
62
+ const res = await this.walletQuery.getUtxos([addr]);
63
+ if (!res.ok) throw new Error(res.error || "Degraded query result");
64
+ return res.utxos[addr] || [];
65
+ },
66
+ async () => this.address(),
67
+ async (inputs: number, outputs: number) => BigInt(inputs * 1000 + outputs * 1000), // Dummy fee estimation
68
+ utxoStore
69
+ );
70
+ }
71
+
72
+ public static open(name: string, options: WalletToolkitOptions = {}): WalletToolkit {
73
+ const store = new WalletStateStoreJson({ filePath: options.storePath || 'default.json' });
74
+ return new WalletToolkit(name, store, options);
75
+ }
76
+
77
+ public async create(): Promise<void> {
78
+ this.walletManager.create({ walletId: this.name });
79
+ }
80
+
81
+ public async address(): Promise<string> {
82
+ // High level facade: gets receive address
83
+ const seedRef = this.walletManager.getSeedRef(this.name);
84
+ return this.addressManager.derive({
85
+ seedRef,
86
+ accountIndex: 0,
87
+ chain: 'receive',
88
+ addressIndex: 0,
89
+ network: this.options.network || 'simnet'
90
+ }).address;
91
+ }
92
+
93
+ public async balance(): Promise<bigint> {
94
+ const addr = await this.address();
95
+ return (await this.walletQuery.getBalance([addr])) as any;
96
+ }
97
+
98
+ public get utxos(): WalletUtxoApi {
99
+ return this._utxosApi;
100
+ }
101
+
102
+ public async history(): Promise<any[]> {
103
+ const addr = await this.address();
104
+ return (await this.walletQuery.getHistory({ addresses: [addr] })) as any;
105
+ }
106
+
107
+ public async estimateFee(opts: { to: string; amount: bigint; feeRate?: bigint }): Promise<{ fee: bigint; totalOut: bigint; plan: any }> {
108
+ const addr = await this.address();
109
+ const availableUtxos = await this.utxos.list();
110
+
111
+ const plan = buildPaymentPlan({
112
+ fromAddress: addr,
113
+ outputs: [{ address: opts.to, amountSompi: opts.amount }],
114
+ availableUtxos: availableUtxos as any[],
115
+ feeRateSompiPerMass: opts.feeRate || 1n,
116
+ changeAddress: addr
117
+ });
118
+
119
+ return {
120
+ fee: plan.estimatedFeeSompi,
121
+ totalOut: opts.amount + plan.estimatedFeeSompi,
122
+ plan
123
+ };
124
+ }
125
+
126
+ public async planSend(opts: { to: string; amount: bigint; feeRate?: bigint }): Promise<any> {
127
+ try {
128
+ const { plan } = await this.estimateFee(opts);
129
+ metrics.inc("wallet_tx_generated_total");
130
+ logger.info("Wallet tx planned", { to: opts.to });
131
+ return plan;
132
+ } catch (e: any) {
133
+ metrics.inc("wallet_tx_failed_total");
134
+ logger.error("Wallet tx planning failed", { error: e.message });
135
+ throw e;
136
+ }
137
+ }
138
+
139
+ public async sendSimulated(opts: { to: string; amount: bigint; feeRate?: bigint }): Promise<string> {
140
+ const span = tracer.start("wallet.send", { to: opts.to });
141
+ try {
142
+ const plan = await this.planSend(opts);
143
+ const hashStr = `sim_tx_${Date.now()}_${plan.inputs.length}_${plan.outputs.length}`;
144
+ for (const u of plan.inputs) {
145
+ await this.utxos.freeze(`${u.outpoint.transactionId}:${u.outpoint.index}`, 'simulated_send');
146
+ }
147
+ metrics.inc("wallet_tx_submitted_total");
148
+ logger.info("Wallet tx simulated successfully", { txid: hashStr });
149
+ span.end({ txid: hashStr });
150
+ return hashStr;
151
+ } catch (e: any) {
152
+ metrics.inc("wallet_tx_failed_total");
153
+ logger.error("Wallet sendSimulated failed", { error: e.message });
154
+ span.fail(e);
155
+ throw e;
156
+ }
157
+ }
158
+ }
@@ -1,109 +1,140 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { WalletToolkit, PaymentToolkit, IndexerToolkit } from '../src/index.js';
3
- import * as fs from 'node:fs/promises';
4
- import * as path from 'node:path';
5
-
6
- describe('@hardkas/toolkit V1', () => {
7
- const tmpDir = path.join(process.cwd(), '.test-toolkit');
8
-
9
- beforeAll(async () => {
10
- try { await fs.mkdir(tmpDir, { recursive: true }); } catch (e) {}
11
- });
12
-
13
- afterAll(async () => {
14
- try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch (e) {}
15
- });
16
-
17
- describe('WalletToolkit', () => {
18
- it('should instantiate and return address', async () => {
19
- const wallet = WalletToolkit.open('alice', { storePath: path.join(tmpDir, 'wallets.json') });
20
- await wallet.create();
21
- const addr = await wallet.address();
22
- expect(addr).toBeDefined();
23
- expect(typeof addr).toBe('string');
24
- expect(addr.startsWith('kaspasim:') || addr.startsWith('kaspa:')).toBe(true);
25
- });
26
-
27
- it('should estimate fee and plan send', async () => {
28
- const wallet = WalletToolkit.open('alice', { storePath: path.join(tmpDir, 'wallets.json') });
29
- // Mock the internal walletQuery for the test
30
- (wallet as any).walletQuery.getUtxos = async () => [];
31
-
32
- await wallet.create(); // Create it so we can fetch it
33
-
34
- // This is a facade test, we just want to ensure it doesn't crash on wiring
35
- const utxos = await wallet.utxos.list();
36
- expect(Array.isArray(utxos)).toBe(true);
37
-
38
- // To test estimateFee, CoinSelector might throw if not enough funds,
39
- // but we at least know it wired up the query correctly.
40
- });
41
- });
42
-
43
- describe('PaymentToolkit', () => {
44
- it('should create invoice and receipt with persistence', async () => {
45
- const pt = PaymentToolkit.openMerchant('store-1', { storePath: path.join(tmpDir, 'invoices.json') });
46
- const inv = await pt.createInvoice({ amount: 150n, currency: 'KAS' });
47
- expect(inv.id).toBeDefined();
48
- expect(inv.uri).toContain('kaspa:store-1');
49
- expect(inv.uri).toContain('amount=150');
50
- expect(inv.status).toBe('pending');
51
-
52
- const status = await pt.check(inv.id);
53
- expect(status).toBe('pending');
54
-
55
- // Simulate payment
56
- await pt.simulatePay(inv.id);
57
- const paidStatus = await pt.check(inv.id);
58
- expect(paidStatus).toBe('paid');
59
-
60
- const receipt = await pt.receipt(inv.id);
61
- expect(receipt.schema).toBe('paymentReceipt.v1');
62
- expect(receipt.merchantId).toBe('store-1');
63
-
64
- const list = await pt.listInvoices();
65
- expect(list.length).toBe(1);
66
-
67
- const stats = await pt.stats();
68
- expect(stats.totalInvoices).toBe(1);
69
- expect(stats.paidInvoices).toBe(1);
70
- });
71
- });
72
-
73
- describe('JobsToolkit', () => {
74
- it('should initialize and enqueue jobs', async () => {
75
- const { JobsToolkit } = await import('../src/index.js');
76
- const jobs = JobsToolkit.open({ storePath: path.join(tmpDir, 'jobs.json') });
77
- jobs.registerHandler('test-job', async (ctx) => {
78
- ctx.progress.update({ total: 10, processed: 10 });
79
- });
80
- const id = await jobs.enqueue('test-job', {});
81
- expect(id).toBeDefined();
82
-
83
- // Allow a tick for async job to execute
84
- await new Promise(r => setTimeout(r, 50));
85
-
86
- const job = await jobs.getJob(id);
87
- expect(job).toBeDefined();
88
- expect(job?.type).toBe('test-job');
89
- });
90
- });
91
-
92
- describe('IndexerToolkit', () => {
93
- it('should ingest and find artifacts', async () => {
94
- const idx = IndexerToolkit.open({ dataDir: tmpDir });
95
-
96
- await idx.watch('kaspa:store-1'); // should just bind
97
-
98
- await idx.ingestArtifact({
99
- id: '123',
100
- schema: 'test.v1',
101
- tags: ['test:abc']
102
- });
103
-
104
- const found = await idx.findReceipts({ tags: ['test:abc'] });
105
- expect(found.length).toBe(1);
106
- expect(found[0].hash).toBe('123');
107
- });
108
- });
109
- });
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { WalletToolkit, PaymentToolkit, IndexerToolkit } from '../src/index.js';
3
+ import * as fs from 'node:fs/promises';
4
+ import * as path from 'node:path';
5
+
6
+ describe('@hardkas/toolkit V1', () => {
7
+ const tmpDir = path.join(process.cwd(), '.test-toolkit');
8
+
9
+ beforeAll(async () => {
10
+ try { await fs.mkdir(tmpDir, { recursive: true }); } catch (e) {}
11
+ });
12
+
13
+ afterAll(async () => {
14
+ try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch (e) {}
15
+ });
16
+
17
+ describe('WalletToolkit', () => {
18
+ it('should instantiate and return address', async () => {
19
+ const wallet = WalletToolkit.open('alice', { storePath: path.join(tmpDir, 'wallets.json') });
20
+ await wallet.create();
21
+ const addr = await wallet.address();
22
+ expect(addr).toBeDefined();
23
+ expect(typeof addr).toBe('string');
24
+ expect(addr.startsWith('kaspasim:') || addr.startsWith('kaspa:')).toBe(true);
25
+ });
26
+
27
+ it('should estimate fee and plan send', async () => {
28
+ const wallet = WalletToolkit.open('alice', { storePath: path.join(tmpDir, 'wallets.json') });
29
+ // Mock the internal walletQuery for the test
30
+ (wallet as any).walletQuery.getUtxos = async () => ({ ok: true, utxos: {} });
31
+
32
+ await wallet.create(); // Create it so we can fetch it
33
+
34
+ // This is a facade test, we just want to ensure it doesn't crash on wiring
35
+ const utxos = await wallet.utxos.list();
36
+ expect(Array.isArray(utxos)).toBe(true);
37
+
38
+ // To test estimateFee, CoinSelector might throw if not enough funds,
39
+ // but we at least know it wired up the query correctly.
40
+ });
41
+ });
42
+
43
+ describe('PaymentToolkit', () => {
44
+ it('should create invoice and receipt with persistence', async () => {
45
+ const pt = PaymentToolkit.openMerchant('store-1', { storePath: path.join(tmpDir, 'invoices.json') });
46
+ const inv = await pt.createInvoice({ amount: 150n, currency: 'KAS' });
47
+ expect(inv.id).toBeDefined();
48
+ expect(inv.uri).toContain('kaspa:store-1');
49
+ expect(inv.uri).toContain('amount=150');
50
+ expect(inv.status).toBe('pending');
51
+
52
+ const status = await pt.check(inv.id);
53
+ expect(status).toBe('pending');
54
+
55
+ // Simulate payment
56
+ await pt.simulatePay(inv.id);
57
+ const paidStatus = await pt.check(inv.id);
58
+ expect(paidStatus).toBe('paid');
59
+
60
+ const receipt = await pt.receipt(inv.id);
61
+ expect(receipt.schema).toBe('paymentReceipt.v1');
62
+ expect(receipt.merchantId).toBe('store-1');
63
+
64
+ const list = await pt.listInvoices();
65
+ expect(list.length).toBe(1);
66
+
67
+ const stats = await pt.stats();
68
+ expect(stats.totalInvoices).toBe(1);
69
+ expect(stats.paidInvoices).toBe(1);
70
+ });
71
+
72
+ it('should throw when requesting receipt for pending or non-existent invoice', async () => {
73
+ const pt = PaymentToolkit.openMerchant('store-err', { storePath: path.join(tmpDir, 'invoices-err.json') });
74
+ const inv = await pt.createInvoice({ amount: 150n, currency: 'KAS' });
75
+
76
+ await expect(pt.receipt(inv.id)).rejects.toThrow('Invoice is not paid');
77
+ await expect(pt.receipt('non-existent')).rejects.toThrow('Invoice not found');
78
+ });
79
+
80
+ it('should allow creating invoice with amountSompi', async () => {
81
+ const pt = PaymentToolkit.openMerchant('store-sompi', { storePath: path.join(tmpDir, 'invoices-sompi.json') });
82
+ const inv = await pt.createInvoice({ amountSompi: 150000000n }); // 1.5 KAS
83
+
84
+ expect(inv.amount).toBe(1n); // 150000000 / 100000000 = 1 (truncates)
85
+ expect(inv.uri).toContain('amount=1.5');
86
+ });
87
+ });
88
+
89
+ describe('JobsToolkit', () => {
90
+ it('should initialize and enqueue jobs', async () => {
91
+ const { JobsToolkit } = await import('../src/index.js');
92
+ const jobs = JobsToolkit.open({ storePath: path.join(tmpDir, 'jobs.json') });
93
+ jobs.registerHandler('test-job', async (ctx) => {
94
+ ctx.progress.update({ total: 10, processed: 10 });
95
+ });
96
+ const id = await jobs.enqueue('test-job', {});
97
+ expect(id).toBeDefined();
98
+
99
+ // Allow a tick for async job to execute
100
+ await new Promise(r => setTimeout(r, 50));
101
+
102
+ const job = await jobs.getJob(id);
103
+ expect(job).toBeDefined();
104
+ expect(job?.type).toBe('test-job');
105
+ });
106
+ });
107
+
108
+ describe('IndexerToolkit', () => {
109
+ it('should bind mock data to address watch and trigger handler', async () => {
110
+ const idx = await IndexerToolkit.open({ dataDir: tmpDir });
111
+
112
+ let called = false;
113
+ await idx.watch('kaspa:store-1', async (evt) => {
114
+ called = true;
115
+ });
116
+
117
+ // Trigger the internal subscriber handler manually since we don't have the timer easily accessible
118
+ const sub = (idx as any).subscriber;
119
+ const handler = Array.from(sub.activeIntervals.entries())[0];
120
+ // Actually the interval doesn't trigger unless we mock options.source.getUtxos, which is empty here.
121
+ // But we can verify it registers correctly.
122
+ await idx.watch(['kaspa:store-1', 'kaspa:store-2']); // should also work
123
+ });
124
+ it('should ingest and find artifacts', async () => {
125
+ const idx = IndexerToolkit.open({ dataDir: tmpDir });
126
+
127
+ await idx.watch('kaspa:store-1'); // should just bind
128
+
129
+ await idx.ingestArtifact({
130
+ id: '123',
131
+ schema: 'test.v1',
132
+ tags: ['test:abc']
133
+ });
134
+
135
+ const found = await idx.findReceipts({ tags: ['test:abc'] });
136
+ expect(found.length).toBe(1);
137
+ expect(found[0].hash).toBe('123');
138
+ });
139
+ });
140
+ });