@arkade-os/swap 0.0.4 → 0.0.6
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/README.md +201 -40
- package/dist/{chunk-DLM6BVTB.js → chunk-Q4FAYBXS.js} +6 -4
- package/dist/chunk-WGRU2DBF.js +38 -0
- package/dist/index.cjs +211 -93
- package/dist/index.d.cts +47 -151
- package/dist/index.d.ts +47 -151
- package/dist/index.js +125 -41
- package/dist/nostr.cjs +11 -0
- package/dist/nostr.d.cts +17 -4
- package/dist/nostr.d.ts +17 -4
- package/dist/nostr.js +9 -1
- package/dist/repositories/realm/index.cjs +138 -0
- package/dist/repositories/realm/index.d.cts +103 -0
- package/dist/repositories/realm/index.d.ts +103 -0
- package/dist/repositories/realm/index.js +108 -0
- package/dist/repositories/sqlite/index.cjs +163 -0
- package/dist/repositories/sqlite/index.d.cts +63 -0
- package/dist/repositories/sqlite/index.d.ts +63 -0
- package/dist/repositories/sqlite/index.js +138 -0
- package/dist/repository-BwnZ8N62.d.cts +236 -0
- package/dist/repository-BwnZ8N62.d.ts +236 -0
- package/dist/{rfq-DjZlesr4.d.cts → rfq-3jWha5xA.d.cts} +40 -16
- package/dist/{rfq-DjZlesr4.d.ts → rfq-3jWha5xA.d.ts} +40 -16
- package/package.json +24 -4
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { RealmLike } from '@arkade-os/sdk/repositories/realm';
|
|
2
|
+
import { A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry } from '../../repository-BwnZ8N62.js';
|
|
3
|
+
import '@arkade-os/solver-discovery';
|
|
4
|
+
import '@arkade-os/sdk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Realm backend for React Native.
|
|
8
|
+
*
|
|
9
|
+
* `realm` is not a dependency of this package: consumers open Realm with the
|
|
10
|
+
* schemas from `./schemas.ts` and pass the instance, validated against the
|
|
11
|
+
* shared `RealmLike` shape from `@arkade-os/sdk`.
|
|
12
|
+
*
|
|
13
|
+
* **Records are serialized as JSON**, whole, into a `data` property —
|
|
14
|
+
* `status` and `createdAt` are mapped out for querying only, so no field of a
|
|
15
|
+
* record can be dropped. That holds for JSON-safe values: a consumer-added
|
|
16
|
+
* `Date` comes back a string and a `bigint` throws on save, unlike the
|
|
17
|
+
* IndexedDB backend's structured clone. `AssetSwap` itself is JSON-safe by
|
|
18
|
+
* design.
|
|
19
|
+
*
|
|
20
|
+
* Realm creates the schemas on open, so there is nothing to initialise. The
|
|
21
|
+
* consumer owns the Realm lifecycle — `[Symbol.asyncDispose]` is a no-op.
|
|
22
|
+
*/
|
|
23
|
+
declare class RealmAssetSwapRepository implements AssetSwapRepository {
|
|
24
|
+
private readonly realm;
|
|
25
|
+
readonly version: 2;
|
|
26
|
+
constructor(realm: RealmLike);
|
|
27
|
+
saveSwap(swap: AssetSwap): Promise<void>;
|
|
28
|
+
getAllSwaps(): Promise<AssetSwap[]>;
|
|
29
|
+
getScannedTxids(): Promise<Set<string>>;
|
|
30
|
+
markTxidsScanned(txids: Iterable<string>): Promise<void>;
|
|
31
|
+
getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
|
|
32
|
+
saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
|
|
33
|
+
/** All three schemas in one write: clearing swaps but keeping scanned txids
|
|
34
|
+
* would leave the restore scan permanently skipping those funding txs, so
|
|
35
|
+
* a partial clear must not be observable. */
|
|
36
|
+
clear(): Promise<void>;
|
|
37
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Realm object schemas for the asset-swap repository.
|
|
42
|
+
*
|
|
43
|
+
* The names land in the **consuming application's** schema namespace, next to
|
|
44
|
+
* its own models and the SDK's `Ark*` / the Boltz plugin's `Boltz*`, so they
|
|
45
|
+
* are prefixed with `ArkadeAssetSwap`. Unlike the SQLite backend there is no
|
|
46
|
+
* prefix option: a Realm schema name is baked into the schema objects the
|
|
47
|
+
* consumer registers and into every `realm.objects(…)` call here.
|
|
48
|
+
*
|
|
49
|
+
* Since `realm` is not a dependency of this package, schemas are plain JS
|
|
50
|
+
* objects conforming to Realm's ObjectSchema shape. They are new, so a consumer
|
|
51
|
+
* adds them to its Realm config and bumps its own `schemaVersion`; no migration
|
|
52
|
+
* helper ships here.
|
|
53
|
+
*/
|
|
54
|
+
declare const ArkadeAssetSwapSchema: {
|
|
55
|
+
name: string;
|
|
56
|
+
primaryKey: string;
|
|
57
|
+
properties: {
|
|
58
|
+
id: string;
|
|
59
|
+
status: string;
|
|
60
|
+
createdAt: string;
|
|
61
|
+
data: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
declare const ArkadeAssetSwapScannedTxidSchema: {
|
|
65
|
+
name: string;
|
|
66
|
+
primaryKey: string;
|
|
67
|
+
properties: {
|
|
68
|
+
txid: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
declare const ArkadeAssetSwapMarketsCacheSchema: {
|
|
72
|
+
name: string;
|
|
73
|
+
primaryKey: string;
|
|
74
|
+
properties: {
|
|
75
|
+
key: string;
|
|
76
|
+
data: string;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
declare const AssetSwapRealmSchemas: ({
|
|
80
|
+
name: string;
|
|
81
|
+
primaryKey: string;
|
|
82
|
+
properties: {
|
|
83
|
+
id: string;
|
|
84
|
+
status: string;
|
|
85
|
+
createdAt: string;
|
|
86
|
+
data: string;
|
|
87
|
+
};
|
|
88
|
+
} | {
|
|
89
|
+
name: string;
|
|
90
|
+
primaryKey: string;
|
|
91
|
+
properties: {
|
|
92
|
+
txid: string;
|
|
93
|
+
};
|
|
94
|
+
} | {
|
|
95
|
+
name: string;
|
|
96
|
+
primaryKey: string;
|
|
97
|
+
properties: {
|
|
98
|
+
key: string;
|
|
99
|
+
data: string;
|
|
100
|
+
};
|
|
101
|
+
})[];
|
|
102
|
+
|
|
103
|
+
export { ArkadeAssetSwapMarketsCacheSchema, ArkadeAssetSwapScannedTxidSchema, ArkadeAssetSwapSchema, AssetSwapRealmSchemas, RealmAssetSwapRepository };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
marketsCacheKey
|
|
3
|
+
} from "../../chunk-WGRU2DBF.js";
|
|
4
|
+
|
|
5
|
+
// src/repositories/realm/repository.ts
|
|
6
|
+
var SWAPS = "ArkadeAssetSwap";
|
|
7
|
+
var SCANNED = "ArkadeAssetSwapScannedTxid";
|
|
8
|
+
var MARKETS = "ArkadeAssetSwapMarketsCache";
|
|
9
|
+
var RealmAssetSwapRepository = class {
|
|
10
|
+
constructor(realm) {
|
|
11
|
+
this.realm = realm;
|
|
12
|
+
}
|
|
13
|
+
realm;
|
|
14
|
+
version = 2;
|
|
15
|
+
async saveSwap(swap) {
|
|
16
|
+
this.realm.write(() => {
|
|
17
|
+
this.realm.create(
|
|
18
|
+
SWAPS,
|
|
19
|
+
{
|
|
20
|
+
id: swap.id,
|
|
21
|
+
status: swap.status,
|
|
22
|
+
createdAt: swap.createdAt,
|
|
23
|
+
data: JSON.stringify(swap)
|
|
24
|
+
},
|
|
25
|
+
"modified"
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async getAllSwaps() {
|
|
30
|
+
return [...this.realm.objects(SWAPS)].map(
|
|
31
|
+
(o) => JSON.parse(o.data)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
async getScannedTxids() {
|
|
35
|
+
return new Set([...this.realm.objects(SCANNED)].map((o) => o.txid));
|
|
36
|
+
}
|
|
37
|
+
async markTxidsScanned(txids) {
|
|
38
|
+
this.realm.write(() => {
|
|
39
|
+
for (const txid of txids) this.realm.create(SCANNED, { txid }, "modified");
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async getCachedMarkets(network, registry) {
|
|
43
|
+
const [row] = [
|
|
44
|
+
...this.realm.objects(MARKETS).filtered("key == $0", marketsCacheKey(network, registry))
|
|
45
|
+
];
|
|
46
|
+
return row ? JSON.parse(row.data) : void 0;
|
|
47
|
+
}
|
|
48
|
+
async saveCachedMarkets(network, registry, entry) {
|
|
49
|
+
this.realm.write(() => {
|
|
50
|
+
this.realm.create(
|
|
51
|
+
MARKETS,
|
|
52
|
+
{ key: marketsCacheKey(network, registry), data: JSON.stringify(entry) },
|
|
53
|
+
"modified"
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** All three schemas in one write: clearing swaps but keeping scanned txids
|
|
58
|
+
* would leave the restore scan permanently skipping those funding txs, so
|
|
59
|
+
* a partial clear must not be observable. */
|
|
60
|
+
async clear() {
|
|
61
|
+
this.realm.write(() => {
|
|
62
|
+
for (const name of [SWAPS, SCANNED, MARKETS]) {
|
|
63
|
+
this.realm.delete(this.realm.objects(name));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async [Symbol.asyncDispose]() {
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/repositories/realm/schemas.ts
|
|
72
|
+
var ArkadeAssetSwapSchema = {
|
|
73
|
+
name: "ArkadeAssetSwap",
|
|
74
|
+
primaryKey: "id",
|
|
75
|
+
properties: {
|
|
76
|
+
id: "string",
|
|
77
|
+
status: "string",
|
|
78
|
+
createdAt: "int",
|
|
79
|
+
data: "string"
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var ArkadeAssetSwapScannedTxidSchema = {
|
|
83
|
+
name: "ArkadeAssetSwapScannedTxid",
|
|
84
|
+
primaryKey: "txid",
|
|
85
|
+
properties: {
|
|
86
|
+
txid: "string"
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var ArkadeAssetSwapMarketsCacheSchema = {
|
|
90
|
+
name: "ArkadeAssetSwapMarketsCache",
|
|
91
|
+
primaryKey: "key",
|
|
92
|
+
properties: {
|
|
93
|
+
key: "string",
|
|
94
|
+
data: "string"
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var AssetSwapRealmSchemas = [
|
|
98
|
+
ArkadeAssetSwapSchema,
|
|
99
|
+
ArkadeAssetSwapScannedTxidSchema,
|
|
100
|
+
ArkadeAssetSwapMarketsCacheSchema
|
|
101
|
+
];
|
|
102
|
+
export {
|
|
103
|
+
ArkadeAssetSwapMarketsCacheSchema,
|
|
104
|
+
ArkadeAssetSwapScannedTxidSchema,
|
|
105
|
+
ArkadeAssetSwapSchema,
|
|
106
|
+
AssetSwapRealmSchemas,
|
|
107
|
+
RealmAssetSwapRepository
|
|
108
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/repositories/sqlite/index.ts
|
|
21
|
+
var sqlite_exports = {};
|
|
22
|
+
__export(sqlite_exports, {
|
|
23
|
+
SQLiteAssetSwapRepository: () => SQLiteAssetSwapRepository
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(sqlite_exports);
|
|
26
|
+
|
|
27
|
+
// src/repositories/sqlite/repository.ts
|
|
28
|
+
var import_sqlite = require("@arkade-os/sdk/repositories/sqlite");
|
|
29
|
+
|
|
30
|
+
// src/repository.ts
|
|
31
|
+
var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
|
|
32
|
+
|
|
33
|
+
// src/repositories/sqlite/repository.ts
|
|
34
|
+
var DEFAULT_PREFIX = "arkade_";
|
|
35
|
+
var INSERT_CHUNK = 500;
|
|
36
|
+
var SQLiteAssetSwapRepository = class {
|
|
37
|
+
constructor(db, options) {
|
|
38
|
+
this.db = db;
|
|
39
|
+
this.prefix = (0, import_sqlite.sanitizeTablePrefix)(options?.prefix ?? DEFAULT_PREFIX);
|
|
40
|
+
this.swaps = `${this.prefix}asset_swaps`;
|
|
41
|
+
this.scanned = `${this.prefix}asset_swap_scanned_txids`;
|
|
42
|
+
this.markets = `${this.prefix}asset_swap_markets`;
|
|
43
|
+
}
|
|
44
|
+
db;
|
|
45
|
+
version = 2;
|
|
46
|
+
initPromise = null;
|
|
47
|
+
prefix;
|
|
48
|
+
swaps;
|
|
49
|
+
scanned;
|
|
50
|
+
markets;
|
|
51
|
+
/** A rejected init is not cached. The DDL runs in a transaction on a shared
|
|
52
|
+
* connection, so it can fail for reasons that pass — a `SQLITE_BUSY` on
|
|
53
|
+
* `BEGIN IMMEDIATE`, a neighbour's rollback — and caching that rejection
|
|
54
|
+
* would strand the instance unusable for the life of the process. Same rule
|
|
55
|
+
* as the IndexedDB backend's `ensureDb`. */
|
|
56
|
+
ensureInit() {
|
|
57
|
+
return this.initPromise ??= this.init().catch((err) => {
|
|
58
|
+
this.initPromise = null;
|
|
59
|
+
throw err;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** The DDL is transactional in SQLite too: run raw on a shared connection it
|
|
63
|
+
* would join a neighbour's open transaction and vanish with its rollback,
|
|
64
|
+
* while `initPromise` stayed resolved and every later statement failed with
|
|
65
|
+
* `no such table`. */
|
|
66
|
+
async init() {
|
|
67
|
+
await this.withTx(async () => {
|
|
68
|
+
await this.db.run(`CREATE TABLE IF NOT EXISTS ${this.swaps} (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
status TEXT NOT NULL,
|
|
71
|
+
created_at INTEGER NOT NULL,
|
|
72
|
+
data TEXT NOT NULL
|
|
73
|
+
)`);
|
|
74
|
+
await this.db.run(
|
|
75
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.prefix}asset_swaps_status ON ${this.swaps} (status)`
|
|
76
|
+
);
|
|
77
|
+
await this.db.run(
|
|
78
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.prefix}asset_swaps_created_at ON ${this.swaps} (created_at)`
|
|
79
|
+
);
|
|
80
|
+
await this.db.run(`CREATE TABLE IF NOT EXISTS ${this.scanned} (txid TEXT PRIMARY KEY)`);
|
|
81
|
+
await this.db.run(
|
|
82
|
+
`CREATE TABLE IF NOT EXISTS ${this.markets} (cache_key TEXT PRIMARY KEY, data TEXT NOT NULL)`
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Every write, including the single-statement ones. The chain serializes
|
|
87
|
+
* transaction *blocks*, not bare statements: one issued between a
|
|
88
|
+
* neighbour's BEGIN IMMEDIATE and its COMMIT becomes part of that
|
|
89
|
+
* transaction and dies with it. `fn` must issue only raw db calls — no
|
|
90
|
+
* repository method, no `ensureInit` — since `runInTransaction` cannot
|
|
91
|
+
* nest. */
|
|
92
|
+
withTx(fn) {
|
|
93
|
+
return (0, import_sqlite.runInTransaction)(this.db, fn);
|
|
94
|
+
}
|
|
95
|
+
async saveSwap(swap) {
|
|
96
|
+
await this.ensureInit();
|
|
97
|
+
await this.withTx(async () => {
|
|
98
|
+
await this.db.run(
|
|
99
|
+
`INSERT OR REPLACE INTO ${this.swaps} (id, status, created_at, data)
|
|
100
|
+
VALUES (?, ?, ?, ?)`,
|
|
101
|
+
[swap.id, swap.status, swap.createdAt, JSON.stringify(swap)]
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async getAllSwaps() {
|
|
106
|
+
await this.ensureInit();
|
|
107
|
+
const rows = await this.db.all(`SELECT data FROM ${this.swaps}`);
|
|
108
|
+
return rows.map((r) => JSON.parse(r.data));
|
|
109
|
+
}
|
|
110
|
+
async getScannedTxids() {
|
|
111
|
+
await this.ensureInit();
|
|
112
|
+
const rows = await this.db.all(`SELECT txid FROM ${this.scanned}`);
|
|
113
|
+
return new Set(rows.map((r) => r.txid));
|
|
114
|
+
}
|
|
115
|
+
async markTxidsScanned(txids) {
|
|
116
|
+
await this.ensureInit();
|
|
117
|
+
const all = [...txids];
|
|
118
|
+
if (all.length === 0) return;
|
|
119
|
+
await this.withTx(async () => {
|
|
120
|
+
for (let i = 0; i < all.length; i += INSERT_CHUNK) {
|
|
121
|
+
const chunk = all.slice(i, i + INSERT_CHUNK);
|
|
122
|
+
await this.db.run(
|
|
123
|
+
`INSERT OR IGNORE INTO ${this.scanned} (txid) VALUES ${chunk.map(() => "(?)").join(", ")}`,
|
|
124
|
+
chunk
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async getCachedMarkets(network, registry) {
|
|
130
|
+
await this.ensureInit();
|
|
131
|
+
const row = await this.db.get(
|
|
132
|
+
`SELECT data FROM ${this.markets} WHERE cache_key = ?`,
|
|
133
|
+
[marketsCacheKey(network, registry)]
|
|
134
|
+
);
|
|
135
|
+
return row ? JSON.parse(row.data) : void 0;
|
|
136
|
+
}
|
|
137
|
+
async saveCachedMarkets(network, registry, entry) {
|
|
138
|
+
await this.ensureInit();
|
|
139
|
+
await this.withTx(async () => {
|
|
140
|
+
await this.db.run(
|
|
141
|
+
`INSERT OR REPLACE INTO ${this.markets} (cache_key, data) VALUES (?, ?)`,
|
|
142
|
+
[marketsCacheKey(network, registry), JSON.stringify(entry)]
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** All three tables in one transaction: clearing swaps but keeping scanned
|
|
147
|
+
* txids would leave the restore scan permanently skipping those funding
|
|
148
|
+
* txs, so a partial clear must not be observable. */
|
|
149
|
+
async clear() {
|
|
150
|
+
await this.ensureInit();
|
|
151
|
+
await this.withTx(async () => {
|
|
152
|
+
await this.db.run(`DELETE FROM ${this.swaps}`);
|
|
153
|
+
await this.db.run(`DELETE FROM ${this.scanned}`);
|
|
154
|
+
await this.db.run(`DELETE FROM ${this.markets}`);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async [Symbol.asyncDispose]() {
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
161
|
+
0 && (module.exports = {
|
|
162
|
+
SQLiteAssetSwapRepository
|
|
163
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SQLExecutor } from '@arkade-os/sdk/repositories/sqlite';
|
|
2
|
+
import { A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry } from '../../repository-BwnZ8N62.cjs';
|
|
3
|
+
import '@arkade-os/solver-discovery';
|
|
4
|
+
import '@arkade-os/sdk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SQLite backend over the SDK's `SQLExecutor`, so any driver plugs in
|
|
8
|
+
* (expo-sqlite on React Native, better-sqlite3, node:sqlite).
|
|
9
|
+
*
|
|
10
|
+
* **Records are serialized as JSON**, whole, into a `data` column — `status`
|
|
11
|
+
* and `created_at` are mapped out for querying only, so no field of a record
|
|
12
|
+
* can be dropped. That holds for JSON-safe values: a consumer-added `Date`
|
|
13
|
+
* comes back a string and a `bigint` throws on save, unlike the IndexedDB
|
|
14
|
+
* backend's structured clone. `AssetSwap` itself is JSON-safe by design.
|
|
15
|
+
*
|
|
16
|
+
* Tables are created lazily on first operation. The consumer owns the
|
|
17
|
+
* `SQLExecutor` lifecycle — `[Symbol.asyncDispose]` is a no-op — and must pass
|
|
18
|
+
* the **same executor instance** to every repository on the database: the
|
|
19
|
+
* write chain is keyed by that object, and a per-repository literal splits it.
|
|
20
|
+
*/
|
|
21
|
+
declare class SQLiteAssetSwapRepository implements AssetSwapRepository {
|
|
22
|
+
private readonly db;
|
|
23
|
+
readonly version: 2;
|
|
24
|
+
private initPromise;
|
|
25
|
+
private readonly prefix;
|
|
26
|
+
private readonly swaps;
|
|
27
|
+
private readonly scanned;
|
|
28
|
+
private readonly markets;
|
|
29
|
+
constructor(db: SQLExecutor, options?: {
|
|
30
|
+
prefix?: string;
|
|
31
|
+
});
|
|
32
|
+
/** A rejected init is not cached. The DDL runs in a transaction on a shared
|
|
33
|
+
* connection, so it can fail for reasons that pass — a `SQLITE_BUSY` on
|
|
34
|
+
* `BEGIN IMMEDIATE`, a neighbour's rollback — and caching that rejection
|
|
35
|
+
* would strand the instance unusable for the life of the process. Same rule
|
|
36
|
+
* as the IndexedDB backend's `ensureDb`. */
|
|
37
|
+
private ensureInit;
|
|
38
|
+
/** The DDL is transactional in SQLite too: run raw on a shared connection it
|
|
39
|
+
* would join a neighbour's open transaction and vanish with its rollback,
|
|
40
|
+
* while `initPromise` stayed resolved and every later statement failed with
|
|
41
|
+
* `no such table`. */
|
|
42
|
+
private init;
|
|
43
|
+
/** Every write, including the single-statement ones. The chain serializes
|
|
44
|
+
* transaction *blocks*, not bare statements: one issued between a
|
|
45
|
+
* neighbour's BEGIN IMMEDIATE and its COMMIT becomes part of that
|
|
46
|
+
* transaction and dies with it. `fn` must issue only raw db calls — no
|
|
47
|
+
* repository method, no `ensureInit` — since `runInTransaction` cannot
|
|
48
|
+
* nest. */
|
|
49
|
+
private withTx;
|
|
50
|
+
saveSwap(swap: AssetSwap): Promise<void>;
|
|
51
|
+
getAllSwaps(): Promise<AssetSwap[]>;
|
|
52
|
+
getScannedTxids(): Promise<Set<string>>;
|
|
53
|
+
markTxidsScanned(txids: Iterable<string>): Promise<void>;
|
|
54
|
+
getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
|
|
55
|
+
saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
|
|
56
|
+
/** All three tables in one transaction: clearing swaps but keeping scanned
|
|
57
|
+
* txids would leave the restore scan permanently skipping those funding
|
|
58
|
+
* txs, so a partial clear must not be observable. */
|
|
59
|
+
clear(): Promise<void>;
|
|
60
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { SQLiteAssetSwapRepository };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SQLExecutor } from '@arkade-os/sdk/repositories/sqlite';
|
|
2
|
+
import { A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry } from '../../repository-BwnZ8N62.js';
|
|
3
|
+
import '@arkade-os/solver-discovery';
|
|
4
|
+
import '@arkade-os/sdk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SQLite backend over the SDK's `SQLExecutor`, so any driver plugs in
|
|
8
|
+
* (expo-sqlite on React Native, better-sqlite3, node:sqlite).
|
|
9
|
+
*
|
|
10
|
+
* **Records are serialized as JSON**, whole, into a `data` column — `status`
|
|
11
|
+
* and `created_at` are mapped out for querying only, so no field of a record
|
|
12
|
+
* can be dropped. That holds for JSON-safe values: a consumer-added `Date`
|
|
13
|
+
* comes back a string and a `bigint` throws on save, unlike the IndexedDB
|
|
14
|
+
* backend's structured clone. `AssetSwap` itself is JSON-safe by design.
|
|
15
|
+
*
|
|
16
|
+
* Tables are created lazily on first operation. The consumer owns the
|
|
17
|
+
* `SQLExecutor` lifecycle — `[Symbol.asyncDispose]` is a no-op — and must pass
|
|
18
|
+
* the **same executor instance** to every repository on the database: the
|
|
19
|
+
* write chain is keyed by that object, and a per-repository literal splits it.
|
|
20
|
+
*/
|
|
21
|
+
declare class SQLiteAssetSwapRepository implements AssetSwapRepository {
|
|
22
|
+
private readonly db;
|
|
23
|
+
readonly version: 2;
|
|
24
|
+
private initPromise;
|
|
25
|
+
private readonly prefix;
|
|
26
|
+
private readonly swaps;
|
|
27
|
+
private readonly scanned;
|
|
28
|
+
private readonly markets;
|
|
29
|
+
constructor(db: SQLExecutor, options?: {
|
|
30
|
+
prefix?: string;
|
|
31
|
+
});
|
|
32
|
+
/** A rejected init is not cached. The DDL runs in a transaction on a shared
|
|
33
|
+
* connection, so it can fail for reasons that pass — a `SQLITE_BUSY` on
|
|
34
|
+
* `BEGIN IMMEDIATE`, a neighbour's rollback — and caching that rejection
|
|
35
|
+
* would strand the instance unusable for the life of the process. Same rule
|
|
36
|
+
* as the IndexedDB backend's `ensureDb`. */
|
|
37
|
+
private ensureInit;
|
|
38
|
+
/** The DDL is transactional in SQLite too: run raw on a shared connection it
|
|
39
|
+
* would join a neighbour's open transaction and vanish with its rollback,
|
|
40
|
+
* while `initPromise` stayed resolved and every later statement failed with
|
|
41
|
+
* `no such table`. */
|
|
42
|
+
private init;
|
|
43
|
+
/** Every write, including the single-statement ones. The chain serializes
|
|
44
|
+
* transaction *blocks*, not bare statements: one issued between a
|
|
45
|
+
* neighbour's BEGIN IMMEDIATE and its COMMIT becomes part of that
|
|
46
|
+
* transaction and dies with it. `fn` must issue only raw db calls — no
|
|
47
|
+
* repository method, no `ensureInit` — since `runInTransaction` cannot
|
|
48
|
+
* nest. */
|
|
49
|
+
private withTx;
|
|
50
|
+
saveSwap(swap: AssetSwap): Promise<void>;
|
|
51
|
+
getAllSwaps(): Promise<AssetSwap[]>;
|
|
52
|
+
getScannedTxids(): Promise<Set<string>>;
|
|
53
|
+
markTxidsScanned(txids: Iterable<string>): Promise<void>;
|
|
54
|
+
getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
|
|
55
|
+
saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
|
|
56
|
+
/** All three tables in one transaction: clearing swaps but keeping scanned
|
|
57
|
+
* txids would leave the restore scan permanently skipping those funding
|
|
58
|
+
* txs, so a partial clear must not be observable. */
|
|
59
|
+
clear(): Promise<void>;
|
|
60
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { SQLiteAssetSwapRepository };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
marketsCacheKey
|
|
3
|
+
} from "../../chunk-WGRU2DBF.js";
|
|
4
|
+
|
|
5
|
+
// src/repositories/sqlite/repository.ts
|
|
6
|
+
import {
|
|
7
|
+
runInTransaction,
|
|
8
|
+
sanitizeTablePrefix
|
|
9
|
+
} from "@arkade-os/sdk/repositories/sqlite";
|
|
10
|
+
var DEFAULT_PREFIX = "arkade_";
|
|
11
|
+
var INSERT_CHUNK = 500;
|
|
12
|
+
var SQLiteAssetSwapRepository = class {
|
|
13
|
+
constructor(db, options) {
|
|
14
|
+
this.db = db;
|
|
15
|
+
this.prefix = sanitizeTablePrefix(options?.prefix ?? DEFAULT_PREFIX);
|
|
16
|
+
this.swaps = `${this.prefix}asset_swaps`;
|
|
17
|
+
this.scanned = `${this.prefix}asset_swap_scanned_txids`;
|
|
18
|
+
this.markets = `${this.prefix}asset_swap_markets`;
|
|
19
|
+
}
|
|
20
|
+
db;
|
|
21
|
+
version = 2;
|
|
22
|
+
initPromise = null;
|
|
23
|
+
prefix;
|
|
24
|
+
swaps;
|
|
25
|
+
scanned;
|
|
26
|
+
markets;
|
|
27
|
+
/** A rejected init is not cached. The DDL runs in a transaction on a shared
|
|
28
|
+
* connection, so it can fail for reasons that pass — a `SQLITE_BUSY` on
|
|
29
|
+
* `BEGIN IMMEDIATE`, a neighbour's rollback — and caching that rejection
|
|
30
|
+
* would strand the instance unusable for the life of the process. Same rule
|
|
31
|
+
* as the IndexedDB backend's `ensureDb`. */
|
|
32
|
+
ensureInit() {
|
|
33
|
+
return this.initPromise ??= this.init().catch((err) => {
|
|
34
|
+
this.initPromise = null;
|
|
35
|
+
throw err;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** The DDL is transactional in SQLite too: run raw on a shared connection it
|
|
39
|
+
* would join a neighbour's open transaction and vanish with its rollback,
|
|
40
|
+
* while `initPromise` stayed resolved and every later statement failed with
|
|
41
|
+
* `no such table`. */
|
|
42
|
+
async init() {
|
|
43
|
+
await this.withTx(async () => {
|
|
44
|
+
await this.db.run(`CREATE TABLE IF NOT EXISTS ${this.swaps} (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
status TEXT NOT NULL,
|
|
47
|
+
created_at INTEGER NOT NULL,
|
|
48
|
+
data TEXT NOT NULL
|
|
49
|
+
)`);
|
|
50
|
+
await this.db.run(
|
|
51
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.prefix}asset_swaps_status ON ${this.swaps} (status)`
|
|
52
|
+
);
|
|
53
|
+
await this.db.run(
|
|
54
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.prefix}asset_swaps_created_at ON ${this.swaps} (created_at)`
|
|
55
|
+
);
|
|
56
|
+
await this.db.run(`CREATE TABLE IF NOT EXISTS ${this.scanned} (txid TEXT PRIMARY KEY)`);
|
|
57
|
+
await this.db.run(
|
|
58
|
+
`CREATE TABLE IF NOT EXISTS ${this.markets} (cache_key TEXT PRIMARY KEY, data TEXT NOT NULL)`
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Every write, including the single-statement ones. The chain serializes
|
|
63
|
+
* transaction *blocks*, not bare statements: one issued between a
|
|
64
|
+
* neighbour's BEGIN IMMEDIATE and its COMMIT becomes part of that
|
|
65
|
+
* transaction and dies with it. `fn` must issue only raw db calls — no
|
|
66
|
+
* repository method, no `ensureInit` — since `runInTransaction` cannot
|
|
67
|
+
* nest. */
|
|
68
|
+
withTx(fn) {
|
|
69
|
+
return runInTransaction(this.db, fn);
|
|
70
|
+
}
|
|
71
|
+
async saveSwap(swap) {
|
|
72
|
+
await this.ensureInit();
|
|
73
|
+
await this.withTx(async () => {
|
|
74
|
+
await this.db.run(
|
|
75
|
+
`INSERT OR REPLACE INTO ${this.swaps} (id, status, created_at, data)
|
|
76
|
+
VALUES (?, ?, ?, ?)`,
|
|
77
|
+
[swap.id, swap.status, swap.createdAt, JSON.stringify(swap)]
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async getAllSwaps() {
|
|
82
|
+
await this.ensureInit();
|
|
83
|
+
const rows = await this.db.all(`SELECT data FROM ${this.swaps}`);
|
|
84
|
+
return rows.map((r) => JSON.parse(r.data));
|
|
85
|
+
}
|
|
86
|
+
async getScannedTxids() {
|
|
87
|
+
await this.ensureInit();
|
|
88
|
+
const rows = await this.db.all(`SELECT txid FROM ${this.scanned}`);
|
|
89
|
+
return new Set(rows.map((r) => r.txid));
|
|
90
|
+
}
|
|
91
|
+
async markTxidsScanned(txids) {
|
|
92
|
+
await this.ensureInit();
|
|
93
|
+
const all = [...txids];
|
|
94
|
+
if (all.length === 0) return;
|
|
95
|
+
await this.withTx(async () => {
|
|
96
|
+
for (let i = 0; i < all.length; i += INSERT_CHUNK) {
|
|
97
|
+
const chunk = all.slice(i, i + INSERT_CHUNK);
|
|
98
|
+
await this.db.run(
|
|
99
|
+
`INSERT OR IGNORE INTO ${this.scanned} (txid) VALUES ${chunk.map(() => "(?)").join(", ")}`,
|
|
100
|
+
chunk
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async getCachedMarkets(network, registry) {
|
|
106
|
+
await this.ensureInit();
|
|
107
|
+
const row = await this.db.get(
|
|
108
|
+
`SELECT data FROM ${this.markets} WHERE cache_key = ?`,
|
|
109
|
+
[marketsCacheKey(network, registry)]
|
|
110
|
+
);
|
|
111
|
+
return row ? JSON.parse(row.data) : void 0;
|
|
112
|
+
}
|
|
113
|
+
async saveCachedMarkets(network, registry, entry) {
|
|
114
|
+
await this.ensureInit();
|
|
115
|
+
await this.withTx(async () => {
|
|
116
|
+
await this.db.run(
|
|
117
|
+
`INSERT OR REPLACE INTO ${this.markets} (cache_key, data) VALUES (?, ?)`,
|
|
118
|
+
[marketsCacheKey(network, registry), JSON.stringify(entry)]
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** All three tables in one transaction: clearing swaps but keeping scanned
|
|
123
|
+
* txids would leave the restore scan permanently skipping those funding
|
|
124
|
+
* txs, so a partial clear must not be observable. */
|
|
125
|
+
async clear() {
|
|
126
|
+
await this.ensureInit();
|
|
127
|
+
await this.withTx(async () => {
|
|
128
|
+
await this.db.run(`DELETE FROM ${this.swaps}`);
|
|
129
|
+
await this.db.run(`DELETE FROM ${this.scanned}`);
|
|
130
|
+
await this.db.run(`DELETE FROM ${this.markets}`);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async [Symbol.asyncDispose]() {
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
export {
|
|
137
|
+
SQLiteAssetSwapRepository
|
|
138
|
+
};
|