@arkade-os/sdk 0.4.0-next.0 → 0.4.0-next.1
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 +127 -24
- package/dist/cjs/bip322/index.js +270 -0
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/intent/index.js +19 -9
- package/dist/cjs/repositories/indexedDB/contractRepository.js +1 -1
- package/dist/cjs/repositories/indexedDB/db.js +8 -46
- package/dist/cjs/repositories/indexedDB/walletRepository.js +1 -1
- package/dist/cjs/repositories/realm/contractRepository.js +120 -0
- package/dist/cjs/repositories/realm/index.js +9 -0
- package/dist/cjs/repositories/realm/schemas.js +108 -0
- package/dist/cjs/repositories/realm/types.js +7 -0
- package/dist/cjs/repositories/realm/walletRepository.js +273 -0
- package/dist/cjs/repositories/serialization.js +49 -0
- package/dist/cjs/repositories/sqlite/contractRepository.js +139 -0
- package/dist/cjs/repositories/sqlite/index.js +7 -0
- package/dist/cjs/repositories/sqlite/types.js +2 -0
- package/dist/cjs/repositories/sqlite/walletRepository.js +328 -0
- package/dist/cjs/wallet/vtxo-manager.js +7 -0
- package/dist/esm/bip322/index.js +267 -0
- package/dist/esm/index.js +4 -1
- package/dist/esm/intent/index.js +16 -7
- package/dist/esm/repositories/indexedDB/contractRepository.js +1 -1
- package/dist/esm/repositories/indexedDB/db.js +2 -40
- package/dist/esm/repositories/indexedDB/walletRepository.js +1 -1
- package/dist/esm/repositories/realm/contractRepository.js +116 -0
- package/dist/esm/repositories/realm/index.js +3 -0
- package/dist/esm/repositories/realm/schemas.js +105 -0
- package/dist/esm/repositories/realm/types.js +6 -0
- package/dist/esm/repositories/realm/walletRepository.js +269 -0
- package/dist/esm/repositories/serialization.js +40 -0
- package/dist/esm/repositories/sqlite/contractRepository.js +135 -0
- package/dist/esm/repositories/sqlite/index.js +2 -0
- package/dist/esm/repositories/sqlite/types.js +1 -0
- package/dist/esm/repositories/sqlite/walletRepository.js +324 -0
- package/dist/esm/wallet/vtxo-manager.js +7 -0
- package/dist/types/bip322/index.d.ts +55 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/intent/index.d.ts +13 -0
- package/dist/types/repositories/indexedDB/db.d.ts +2 -54
- package/dist/types/repositories/realm/contractRepository.d.ts +24 -0
- package/dist/types/repositories/realm/index.d.ts +4 -0
- package/dist/types/repositories/realm/schemas.d.ts +208 -0
- package/dist/types/repositories/realm/types.d.ts +16 -0
- package/dist/types/repositories/realm/walletRepository.d.ts +31 -0
- package/dist/types/repositories/serialization.d.ts +40 -0
- package/dist/types/repositories/sqlite/contractRepository.d.ts +33 -0
- package/dist/types/repositories/sqlite/index.d.ts +3 -0
- package/dist/types/repositories/sqlite/types.d.ts +18 -0
- package/dist/types/repositories/sqlite/walletRepository.d.ts +40 -0
- package/package.json +18 -14
- package/dist/cjs/adapters/expo-db.js +0 -35
- package/dist/esm/adapters/expo-db.js +0 -27
- package/dist/types/adapters/expo-db.d.ts +0 -7
- /package/dist/cjs/{db → repositories/indexedDB}/manager.js +0 -0
- /package/dist/esm/{db → repositories/indexedDB}/manager.js +0 -0
- /package/dist/types/{db → repositories/indexedDB}/manager.d.ts +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, } from '../serialization.js';
|
|
2
|
+
/**
|
|
3
|
+
* Realm-based implementation of WalletRepository.
|
|
4
|
+
*
|
|
5
|
+
* Consumers must open Realm with the schemas from `./schemas.ts` and pass
|
|
6
|
+
* the instance to the constructor.
|
|
7
|
+
*
|
|
8
|
+
* Realm handles schema creation on open, so `ensureInit()` is a no-op.
|
|
9
|
+
* The consumer owns the Realm lifecycle — `[Symbol.asyncDispose]` is a no-op.
|
|
10
|
+
*/
|
|
11
|
+
export class RealmWalletRepository {
|
|
12
|
+
constructor(realm) {
|
|
13
|
+
this.realm = realm;
|
|
14
|
+
this.version = 1;
|
|
15
|
+
}
|
|
16
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
17
|
+
async ensureInit() {
|
|
18
|
+
// Realm handles schema on open — nothing to initialise.
|
|
19
|
+
}
|
|
20
|
+
async [Symbol.asyncDispose]() {
|
|
21
|
+
// no-op — consumer owns the Realm lifecycle
|
|
22
|
+
}
|
|
23
|
+
// ── Clear ──────────────────────────────────────────────────────────
|
|
24
|
+
async clear() {
|
|
25
|
+
await this.ensureInit();
|
|
26
|
+
this.realm.write(() => {
|
|
27
|
+
this.realm.delete(this.realm.objects("ArkVtxo"));
|
|
28
|
+
this.realm.delete(this.realm.objects("ArkUtxo"));
|
|
29
|
+
this.realm.delete(this.realm.objects("ArkTransaction"));
|
|
30
|
+
this.realm.delete(this.realm.objects("ArkWalletState"));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// ── VTXO management ────────────────────────────────────────────────
|
|
34
|
+
async getVtxos(address) {
|
|
35
|
+
await this.ensureInit();
|
|
36
|
+
const results = this.realm
|
|
37
|
+
.objects("ArkVtxo")
|
|
38
|
+
.filtered("address == $0", address);
|
|
39
|
+
return [...results].map(vtxoObjectToDomain);
|
|
40
|
+
}
|
|
41
|
+
async saveVtxos(address, vtxos) {
|
|
42
|
+
await this.ensureInit();
|
|
43
|
+
this.realm.write(() => {
|
|
44
|
+
for (const vtxo of vtxos) {
|
|
45
|
+
const s = serializeVtxo(vtxo);
|
|
46
|
+
this.realm.create("ArkVtxo", {
|
|
47
|
+
pk: `${s.txid}:${s.vout}`,
|
|
48
|
+
address,
|
|
49
|
+
txid: s.txid,
|
|
50
|
+
vout: s.vout,
|
|
51
|
+
value: s.value,
|
|
52
|
+
tapTree: s.tapTree,
|
|
53
|
+
forfeitCb: s.forfeitTapLeafScript.cb,
|
|
54
|
+
forfeitS: s.forfeitTapLeafScript.s,
|
|
55
|
+
intentCb: s.intentTapLeafScript.cb,
|
|
56
|
+
intentS: s.intentTapLeafScript.s,
|
|
57
|
+
statusJson: JSON.stringify(s.status),
|
|
58
|
+
virtualStatusJson: JSON.stringify(s.virtualStatus),
|
|
59
|
+
createdAt: typeof s.createdAt === "string"
|
|
60
|
+
? s.createdAt
|
|
61
|
+
: s.createdAt instanceof Date
|
|
62
|
+
? s.createdAt.toISOString()
|
|
63
|
+
: new Date(s.createdAt).toISOString(),
|
|
64
|
+
isUnrolled: s.isUnrolled ?? false,
|
|
65
|
+
isSpent: s.isSpent === undefined ? null : s.isSpent,
|
|
66
|
+
spentBy: s.spentBy ?? null,
|
|
67
|
+
settledBy: s.settledBy ?? null,
|
|
68
|
+
arkTxId: s.arkTxId ?? null,
|
|
69
|
+
extraWitnessJson: s.extraWitness
|
|
70
|
+
? JSON.stringify(s.extraWitness)
|
|
71
|
+
: null,
|
|
72
|
+
assetsJson: s.assets ? JSON.stringify(s.assets) : null,
|
|
73
|
+
}, "modified");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async deleteVtxos(address) {
|
|
78
|
+
await this.ensureInit();
|
|
79
|
+
this.realm.write(() => {
|
|
80
|
+
const toDelete = this.realm
|
|
81
|
+
.objects("ArkVtxo")
|
|
82
|
+
.filtered("address == $0", address);
|
|
83
|
+
this.realm.delete(toDelete);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// ── UTXO management ────────────────────────────────────────────────
|
|
87
|
+
async getUtxos(address) {
|
|
88
|
+
await this.ensureInit();
|
|
89
|
+
const results = this.realm
|
|
90
|
+
.objects("ArkUtxo")
|
|
91
|
+
.filtered("address == $0", address);
|
|
92
|
+
return [...results].map(utxoObjectToDomain);
|
|
93
|
+
}
|
|
94
|
+
async saveUtxos(address, utxos) {
|
|
95
|
+
await this.ensureInit();
|
|
96
|
+
this.realm.write(() => {
|
|
97
|
+
for (const utxo of utxos) {
|
|
98
|
+
const s = serializeUtxo(utxo);
|
|
99
|
+
this.realm.create("ArkUtxo", {
|
|
100
|
+
pk: `${s.txid}:${s.vout}`,
|
|
101
|
+
address,
|
|
102
|
+
txid: s.txid,
|
|
103
|
+
vout: s.vout,
|
|
104
|
+
value: s.value,
|
|
105
|
+
tapTree: s.tapTree,
|
|
106
|
+
forfeitCb: s.forfeitTapLeafScript.cb,
|
|
107
|
+
forfeitS: s.forfeitTapLeafScript.s,
|
|
108
|
+
intentCb: s.intentTapLeafScript.cb,
|
|
109
|
+
intentS: s.intentTapLeafScript.s,
|
|
110
|
+
statusJson: JSON.stringify(s.status),
|
|
111
|
+
extraWitnessJson: s.extraWitness
|
|
112
|
+
? JSON.stringify(s.extraWitness)
|
|
113
|
+
: null,
|
|
114
|
+
}, "modified");
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async deleteUtxos(address) {
|
|
119
|
+
await this.ensureInit();
|
|
120
|
+
this.realm.write(() => {
|
|
121
|
+
const toDelete = this.realm
|
|
122
|
+
.objects("ArkUtxo")
|
|
123
|
+
.filtered("address == $0", address);
|
|
124
|
+
this.realm.delete(toDelete);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// ── Transaction history ────────────────────────────────────────────
|
|
128
|
+
async getTransactionHistory(address) {
|
|
129
|
+
await this.ensureInit();
|
|
130
|
+
const results = this.realm
|
|
131
|
+
.objects("ArkTransaction")
|
|
132
|
+
.filtered("address == $0", address);
|
|
133
|
+
const txs = [...results].map(txObjectToDomain);
|
|
134
|
+
txs.sort((a, b) => a.createdAt - b.createdAt);
|
|
135
|
+
return txs;
|
|
136
|
+
}
|
|
137
|
+
async saveTransactions(address, txs) {
|
|
138
|
+
await this.ensureInit();
|
|
139
|
+
this.realm.write(() => {
|
|
140
|
+
for (const tx of txs) {
|
|
141
|
+
this.realm.create("ArkTransaction", {
|
|
142
|
+
pk: `${address}:${tx.key.boardingTxid}:${tx.key.commitmentTxid}:${tx.key.arkTxid}`,
|
|
143
|
+
address,
|
|
144
|
+
boardingTxid: tx.key.boardingTxid,
|
|
145
|
+
commitmentTxid: tx.key.commitmentTxid,
|
|
146
|
+
arkTxid: tx.key.arkTxid,
|
|
147
|
+
type: tx.type,
|
|
148
|
+
amount: tx.amount,
|
|
149
|
+
settled: tx.settled,
|
|
150
|
+
createdAt: tx.createdAt,
|
|
151
|
+
assetsJson: tx.assets
|
|
152
|
+
? JSON.stringify(tx.assets)
|
|
153
|
+
: null,
|
|
154
|
+
}, "modified");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async deleteTransactions(address) {
|
|
159
|
+
await this.ensureInit();
|
|
160
|
+
this.realm.write(() => {
|
|
161
|
+
const toDelete = this.realm
|
|
162
|
+
.objects("ArkTransaction")
|
|
163
|
+
.filtered("address == $0", address);
|
|
164
|
+
this.realm.delete(toDelete);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// ── Wallet state ───────────────────────────────────────────────────
|
|
168
|
+
async getWalletState() {
|
|
169
|
+
await this.ensureInit();
|
|
170
|
+
const results = this.realm
|
|
171
|
+
.objects("ArkWalletState")
|
|
172
|
+
.filtered("key == $0", "state");
|
|
173
|
+
const items = [...results];
|
|
174
|
+
if (items.length === 0)
|
|
175
|
+
return null;
|
|
176
|
+
const obj = items[0];
|
|
177
|
+
const state = {};
|
|
178
|
+
if (obj.lastSyncTime !== null && obj.lastSyncTime !== undefined) {
|
|
179
|
+
state.lastSyncTime = obj.lastSyncTime;
|
|
180
|
+
}
|
|
181
|
+
if (obj.settingsJson) {
|
|
182
|
+
state.settings = JSON.parse(obj.settingsJson);
|
|
183
|
+
}
|
|
184
|
+
return state;
|
|
185
|
+
}
|
|
186
|
+
async saveWalletState(state) {
|
|
187
|
+
await this.ensureInit();
|
|
188
|
+
this.realm.write(() => {
|
|
189
|
+
this.realm.create("ArkWalletState", {
|
|
190
|
+
key: "state",
|
|
191
|
+
lastSyncTime: state.lastSyncTime ?? null,
|
|
192
|
+
settingsJson: state.settings
|
|
193
|
+
? JSON.stringify(state.settings)
|
|
194
|
+
: null,
|
|
195
|
+
}, "modified");
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ── Realm object → Domain converters ─────────────────────────────────────
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
201
|
+
function vtxoObjectToDomain(obj) {
|
|
202
|
+
const serialized = {
|
|
203
|
+
txid: obj.txid,
|
|
204
|
+
vout: obj.vout,
|
|
205
|
+
value: obj.value,
|
|
206
|
+
tapTree: obj.tapTree,
|
|
207
|
+
forfeitTapLeafScript: {
|
|
208
|
+
cb: obj.forfeitCb,
|
|
209
|
+
s: obj.forfeitS,
|
|
210
|
+
},
|
|
211
|
+
intentTapLeafScript: {
|
|
212
|
+
cb: obj.intentCb,
|
|
213
|
+
s: obj.intentS,
|
|
214
|
+
},
|
|
215
|
+
status: JSON.parse(obj.statusJson),
|
|
216
|
+
virtualStatus: JSON.parse(obj.virtualStatusJson),
|
|
217
|
+
createdAt: new Date(obj.createdAt),
|
|
218
|
+
isUnrolled: obj.isUnrolled,
|
|
219
|
+
isSpent: obj.isSpent === null ? undefined : obj.isSpent,
|
|
220
|
+
spentBy: obj.spentBy ?? undefined,
|
|
221
|
+
settledBy: obj.settledBy ?? undefined,
|
|
222
|
+
arkTxId: obj.arkTxId ?? undefined,
|
|
223
|
+
extraWitness: obj.extraWitnessJson
|
|
224
|
+
? JSON.parse(obj.extraWitnessJson)
|
|
225
|
+
: undefined,
|
|
226
|
+
assets: obj.assetsJson ? JSON.parse(obj.assetsJson) : undefined,
|
|
227
|
+
};
|
|
228
|
+
return deserializeVtxo(serialized);
|
|
229
|
+
}
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
231
|
+
function utxoObjectToDomain(obj) {
|
|
232
|
+
const serialized = {
|
|
233
|
+
txid: obj.txid,
|
|
234
|
+
vout: obj.vout,
|
|
235
|
+
value: obj.value,
|
|
236
|
+
tapTree: obj.tapTree,
|
|
237
|
+
forfeitTapLeafScript: {
|
|
238
|
+
cb: obj.forfeitCb,
|
|
239
|
+
s: obj.forfeitS,
|
|
240
|
+
},
|
|
241
|
+
intentTapLeafScript: {
|
|
242
|
+
cb: obj.intentCb,
|
|
243
|
+
s: obj.intentS,
|
|
244
|
+
},
|
|
245
|
+
status: JSON.parse(obj.statusJson),
|
|
246
|
+
extraWitness: obj.extraWitnessJson
|
|
247
|
+
? JSON.parse(obj.extraWitnessJson)
|
|
248
|
+
: undefined,
|
|
249
|
+
};
|
|
250
|
+
return deserializeUtxo(serialized);
|
|
251
|
+
}
|
|
252
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
253
|
+
function txObjectToDomain(obj) {
|
|
254
|
+
const tx = {
|
|
255
|
+
key: {
|
|
256
|
+
boardingTxid: obj.boardingTxid,
|
|
257
|
+
commitmentTxid: obj.commitmentTxid,
|
|
258
|
+
arkTxid: obj.arkTxid,
|
|
259
|
+
},
|
|
260
|
+
type: obj.type,
|
|
261
|
+
amount: obj.amount,
|
|
262
|
+
settled: obj.settled,
|
|
263
|
+
createdAt: obj.createdAt,
|
|
264
|
+
};
|
|
265
|
+
if (obj.assetsJson) {
|
|
266
|
+
tx.assets = JSON.parse(obj.assetsJson);
|
|
267
|
+
}
|
|
268
|
+
return tx;
|
|
269
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { hex } from "@scure/base";
|
|
2
|
+
import { TaprootControlBlock } from "@scure/btc-signer";
|
|
3
|
+
export const serializeTapLeaf = ([cb, s,]) => ({
|
|
4
|
+
cb: hex.encode(TaprootControlBlock.encode(cb)),
|
|
5
|
+
s: hex.encode(s),
|
|
6
|
+
});
|
|
7
|
+
export const serializeVtxo = (v) => ({
|
|
8
|
+
...v,
|
|
9
|
+
tapTree: hex.encode(v.tapTree),
|
|
10
|
+
forfeitTapLeafScript: serializeTapLeaf(v.forfeitTapLeafScript),
|
|
11
|
+
intentTapLeafScript: serializeTapLeaf(v.intentTapLeafScript),
|
|
12
|
+
extraWitness: v.extraWitness?.map(hex.encode),
|
|
13
|
+
});
|
|
14
|
+
export const serializeUtxo = (u) => ({
|
|
15
|
+
...u,
|
|
16
|
+
tapTree: hex.encode(u.tapTree),
|
|
17
|
+
forfeitTapLeafScript: serializeTapLeaf(u.forfeitTapLeafScript),
|
|
18
|
+
intentTapLeafScript: serializeTapLeaf(u.intentTapLeafScript),
|
|
19
|
+
extraWitness: u.extraWitness?.map(hex.encode),
|
|
20
|
+
});
|
|
21
|
+
export const deserializeTapLeaf = (t) => {
|
|
22
|
+
const cb = TaprootControlBlock.decode(hex.decode(t.cb));
|
|
23
|
+
const s = hex.decode(t.s);
|
|
24
|
+
return [cb, s];
|
|
25
|
+
};
|
|
26
|
+
export const deserializeVtxo = (o) => ({
|
|
27
|
+
...o,
|
|
28
|
+
createdAt: new Date(o.createdAt),
|
|
29
|
+
tapTree: hex.decode(o.tapTree),
|
|
30
|
+
forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
|
|
31
|
+
intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
|
|
32
|
+
extraWitness: o.extraWitness?.map(hex.decode),
|
|
33
|
+
});
|
|
34
|
+
export const deserializeUtxo = (o) => ({
|
|
35
|
+
...o,
|
|
36
|
+
tapTree: hex.decode(o.tapTree),
|
|
37
|
+
forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
|
|
38
|
+
intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
|
|
39
|
+
extraWitness: o.extraWitness?.map(hex.decode),
|
|
40
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-based implementation of ContractRepository.
|
|
3
|
+
*
|
|
4
|
+
* Uses the SQLExecutor interface so consumers can plug in any SQLite driver
|
|
5
|
+
* (expo-sqlite, better-sqlite3, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Tables are created lazily on first operation via `ensureInit()`.
|
|
8
|
+
* The consumer owns the SQLExecutor lifecycle — `[Symbol.asyncDispose]` is a no-op.
|
|
9
|
+
*/
|
|
10
|
+
export class SQLiteContractRepository {
|
|
11
|
+
constructor(db, options) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
this.version = 1;
|
|
14
|
+
this.initPromise = null;
|
|
15
|
+
this.prefix = sanitizePrefix(options?.prefix ?? "ark_");
|
|
16
|
+
this.table = `${this.prefix}contracts`;
|
|
17
|
+
}
|
|
18
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
19
|
+
ensureInit() {
|
|
20
|
+
if (!this.initPromise) {
|
|
21
|
+
this.initPromise = this.init();
|
|
22
|
+
}
|
|
23
|
+
return this.initPromise;
|
|
24
|
+
}
|
|
25
|
+
async init() {
|
|
26
|
+
await this.db.run(`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
28
|
+
script TEXT PRIMARY KEY,
|
|
29
|
+
address TEXT NOT NULL,
|
|
30
|
+
type TEXT NOT NULL,
|
|
31
|
+
state TEXT NOT NULL,
|
|
32
|
+
params_json TEXT NOT NULL,
|
|
33
|
+
created_at INTEGER NOT NULL,
|
|
34
|
+
expires_at INTEGER,
|
|
35
|
+
label TEXT,
|
|
36
|
+
metadata_json TEXT
|
|
37
|
+
)
|
|
38
|
+
`);
|
|
39
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_type ON ${this.table} (type)`);
|
|
40
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_state ON ${this.table} (state)`);
|
|
41
|
+
}
|
|
42
|
+
async [Symbol.asyncDispose]() {
|
|
43
|
+
// no-op — consumer owns the SQLExecutor lifecycle
|
|
44
|
+
}
|
|
45
|
+
// ── Clear ──────────────────────────────────────────────────────────
|
|
46
|
+
async clear() {
|
|
47
|
+
await this.ensureInit();
|
|
48
|
+
await this.db.run(`DELETE FROM ${this.table}`);
|
|
49
|
+
}
|
|
50
|
+
// ── Contract management ────────────────────────────────────────────
|
|
51
|
+
async getContracts(filter) {
|
|
52
|
+
await this.ensureInit();
|
|
53
|
+
const conditions = [];
|
|
54
|
+
const params = [];
|
|
55
|
+
if (filter) {
|
|
56
|
+
this.addFilterCondition(conditions, params, "script", filter.script);
|
|
57
|
+
this.addFilterCondition(conditions, params, "state", filter.state);
|
|
58
|
+
this.addFilterCondition(conditions, params, "type", filter.type);
|
|
59
|
+
}
|
|
60
|
+
let sql = `SELECT * FROM ${this.table}`;
|
|
61
|
+
if (conditions.length > 0) {
|
|
62
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
63
|
+
}
|
|
64
|
+
const rows = await this.db.all(sql, params);
|
|
65
|
+
return rows.map(contractRowToDomain);
|
|
66
|
+
}
|
|
67
|
+
async saveContract(contract) {
|
|
68
|
+
await this.ensureInit();
|
|
69
|
+
await this.db.run(`INSERT OR REPLACE INTO ${this.table}
|
|
70
|
+
(script, address, type, state, params_json,
|
|
71
|
+
created_at, expires_at, label, metadata_json)
|
|
72
|
+
VALUES (?, ?, ?, ?, ?,
|
|
73
|
+
?, ?, ?, ?)`, [
|
|
74
|
+
contract.script,
|
|
75
|
+
contract.address,
|
|
76
|
+
contract.type,
|
|
77
|
+
contract.state,
|
|
78
|
+
JSON.stringify(contract.params),
|
|
79
|
+
contract.createdAt,
|
|
80
|
+
contract.expiresAt ?? null,
|
|
81
|
+
contract.label ?? null,
|
|
82
|
+
contract.metadata ? JSON.stringify(contract.metadata) : null,
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
async deleteContract(script) {
|
|
86
|
+
await this.ensureInit();
|
|
87
|
+
await this.db.run(`DELETE FROM ${this.table} WHERE script = ?`, [
|
|
88
|
+
script,
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
92
|
+
addFilterCondition(conditions, params, column, value) {
|
|
93
|
+
if (value === undefined)
|
|
94
|
+
return;
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
if (value.length === 0)
|
|
97
|
+
return;
|
|
98
|
+
const placeholders = value.map(() => "?").join(", ");
|
|
99
|
+
conditions.push(`${column} IN (${placeholders})`);
|
|
100
|
+
params.push(...value);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
conditions.push(`${column} = ?`);
|
|
104
|
+
params.push(value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ── Row → Domain converter ──────────────────────────────────────────────
|
|
109
|
+
const SAFE_PREFIX = /^[a-zA-Z0-9_]+$/;
|
|
110
|
+
function sanitizePrefix(prefix) {
|
|
111
|
+
if (!SAFE_PREFIX.test(prefix)) {
|
|
112
|
+
throw new Error(`Invalid table prefix "${prefix}": only letters, digits, and underscores are allowed`);
|
|
113
|
+
}
|
|
114
|
+
return prefix;
|
|
115
|
+
}
|
|
116
|
+
function contractRowToDomain(row) {
|
|
117
|
+
const contract = {
|
|
118
|
+
script: row.script,
|
|
119
|
+
address: row.address,
|
|
120
|
+
type: row.type,
|
|
121
|
+
state: row.state,
|
|
122
|
+
params: JSON.parse(row.params_json),
|
|
123
|
+
createdAt: row.created_at,
|
|
124
|
+
};
|
|
125
|
+
if (row.expires_at !== null) {
|
|
126
|
+
contract.expiresAt = row.expires_at;
|
|
127
|
+
}
|
|
128
|
+
if (row.label !== null) {
|
|
129
|
+
contract.label = row.label;
|
|
130
|
+
}
|
|
131
|
+
if (row.metadata_json !== null) {
|
|
132
|
+
contract.metadata = JSON.parse(row.metadata_json);
|
|
133
|
+
}
|
|
134
|
+
return contract;
|
|
135
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|