@arkade-os/sdk 0.4.0-next.0 → 0.4.0-next.2
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/serviceWorker/wallet.js +9 -1
- package/dist/cjs/wallet/vtxo-manager.js +7 -0
- package/dist/cjs/worker/messageBus.js +22 -2
- 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/serviceWorker/wallet.js +9 -1
- package/dist/esm/wallet/vtxo-manager.js +7 -0
- package/dist/esm/worker/messageBus.js +22 -2
- 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,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SQLiteContractRepository = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* SQLite-based implementation of ContractRepository.
|
|
6
|
+
*
|
|
7
|
+
* Uses the SQLExecutor interface so consumers can plug in any SQLite driver
|
|
8
|
+
* (expo-sqlite, better-sqlite3, etc.).
|
|
9
|
+
*
|
|
10
|
+
* Tables are created lazily on first operation via `ensureInit()`.
|
|
11
|
+
* The consumer owns the SQLExecutor lifecycle — `[Symbol.asyncDispose]` is a no-op.
|
|
12
|
+
*/
|
|
13
|
+
class SQLiteContractRepository {
|
|
14
|
+
constructor(db, options) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
this.version = 1;
|
|
17
|
+
this.initPromise = null;
|
|
18
|
+
this.prefix = sanitizePrefix(options?.prefix ?? "ark_");
|
|
19
|
+
this.table = `${this.prefix}contracts`;
|
|
20
|
+
}
|
|
21
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
22
|
+
ensureInit() {
|
|
23
|
+
if (!this.initPromise) {
|
|
24
|
+
this.initPromise = this.init();
|
|
25
|
+
}
|
|
26
|
+
return this.initPromise;
|
|
27
|
+
}
|
|
28
|
+
async init() {
|
|
29
|
+
await this.db.run(`
|
|
30
|
+
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
31
|
+
script TEXT PRIMARY KEY,
|
|
32
|
+
address TEXT NOT NULL,
|
|
33
|
+
type TEXT NOT NULL,
|
|
34
|
+
state TEXT NOT NULL,
|
|
35
|
+
params_json TEXT NOT NULL,
|
|
36
|
+
created_at INTEGER NOT NULL,
|
|
37
|
+
expires_at INTEGER,
|
|
38
|
+
label TEXT,
|
|
39
|
+
metadata_json TEXT
|
|
40
|
+
)
|
|
41
|
+
`);
|
|
42
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_type ON ${this.table} (type)`);
|
|
43
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_state ON ${this.table} (state)`);
|
|
44
|
+
}
|
|
45
|
+
async [Symbol.asyncDispose]() {
|
|
46
|
+
// no-op — consumer owns the SQLExecutor lifecycle
|
|
47
|
+
}
|
|
48
|
+
// ── Clear ──────────────────────────────────────────────────────────
|
|
49
|
+
async clear() {
|
|
50
|
+
await this.ensureInit();
|
|
51
|
+
await this.db.run(`DELETE FROM ${this.table}`);
|
|
52
|
+
}
|
|
53
|
+
// ── Contract management ────────────────────────────────────────────
|
|
54
|
+
async getContracts(filter) {
|
|
55
|
+
await this.ensureInit();
|
|
56
|
+
const conditions = [];
|
|
57
|
+
const params = [];
|
|
58
|
+
if (filter) {
|
|
59
|
+
this.addFilterCondition(conditions, params, "script", filter.script);
|
|
60
|
+
this.addFilterCondition(conditions, params, "state", filter.state);
|
|
61
|
+
this.addFilterCondition(conditions, params, "type", filter.type);
|
|
62
|
+
}
|
|
63
|
+
let sql = `SELECT * FROM ${this.table}`;
|
|
64
|
+
if (conditions.length > 0) {
|
|
65
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
66
|
+
}
|
|
67
|
+
const rows = await this.db.all(sql, params);
|
|
68
|
+
return rows.map(contractRowToDomain);
|
|
69
|
+
}
|
|
70
|
+
async saveContract(contract) {
|
|
71
|
+
await this.ensureInit();
|
|
72
|
+
await this.db.run(`INSERT OR REPLACE INTO ${this.table}
|
|
73
|
+
(script, address, type, state, params_json,
|
|
74
|
+
created_at, expires_at, label, metadata_json)
|
|
75
|
+
VALUES (?, ?, ?, ?, ?,
|
|
76
|
+
?, ?, ?, ?)`, [
|
|
77
|
+
contract.script,
|
|
78
|
+
contract.address,
|
|
79
|
+
contract.type,
|
|
80
|
+
contract.state,
|
|
81
|
+
JSON.stringify(contract.params),
|
|
82
|
+
contract.createdAt,
|
|
83
|
+
contract.expiresAt ?? null,
|
|
84
|
+
contract.label ?? null,
|
|
85
|
+
contract.metadata ? JSON.stringify(contract.metadata) : null,
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
async deleteContract(script) {
|
|
89
|
+
await this.ensureInit();
|
|
90
|
+
await this.db.run(`DELETE FROM ${this.table} WHERE script = ?`, [
|
|
91
|
+
script,
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
95
|
+
addFilterCondition(conditions, params, column, value) {
|
|
96
|
+
if (value === undefined)
|
|
97
|
+
return;
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
if (value.length === 0)
|
|
100
|
+
return;
|
|
101
|
+
const placeholders = value.map(() => "?").join(", ");
|
|
102
|
+
conditions.push(`${column} IN (${placeholders})`);
|
|
103
|
+
params.push(...value);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
conditions.push(`${column} = ?`);
|
|
107
|
+
params.push(value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.SQLiteContractRepository = SQLiteContractRepository;
|
|
112
|
+
// ── Row → Domain converter ──────────────────────────────────────────────
|
|
113
|
+
const SAFE_PREFIX = /^[a-zA-Z0-9_]+$/;
|
|
114
|
+
function sanitizePrefix(prefix) {
|
|
115
|
+
if (!SAFE_PREFIX.test(prefix)) {
|
|
116
|
+
throw new Error(`Invalid table prefix "${prefix}": only letters, digits, and underscores are allowed`);
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
}
|
|
120
|
+
function contractRowToDomain(row) {
|
|
121
|
+
const contract = {
|
|
122
|
+
script: row.script,
|
|
123
|
+
address: row.address,
|
|
124
|
+
type: row.type,
|
|
125
|
+
state: row.state,
|
|
126
|
+
params: JSON.parse(row.params_json),
|
|
127
|
+
createdAt: row.created_at,
|
|
128
|
+
};
|
|
129
|
+
if (row.expires_at !== null) {
|
|
130
|
+
contract.expiresAt = row.expires_at;
|
|
131
|
+
}
|
|
132
|
+
if (row.label !== null) {
|
|
133
|
+
contract.label = row.label;
|
|
134
|
+
}
|
|
135
|
+
if (row.metadata_json !== null) {
|
|
136
|
+
contract.metadata = JSON.parse(row.metadata_json);
|
|
137
|
+
}
|
|
138
|
+
return contract;
|
|
139
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SQLiteContractRepository = exports.SQLiteWalletRepository = void 0;
|
|
4
|
+
var walletRepository_1 = require("./walletRepository");
|
|
5
|
+
Object.defineProperty(exports, "SQLiteWalletRepository", { enumerable: true, get: function () { return walletRepository_1.SQLiteWalletRepository; } });
|
|
6
|
+
var contractRepository_1 = require("./contractRepository");
|
|
7
|
+
Object.defineProperty(exports, "SQLiteContractRepository", { enumerable: true, get: function () { return contractRepository_1.SQLiteContractRepository; } });
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SQLiteWalletRepository = void 0;
|
|
4
|
+
const serialization_1 = require("../serialization");
|
|
5
|
+
/**
|
|
6
|
+
* SQLite-based implementation of WalletRepository.
|
|
7
|
+
*
|
|
8
|
+
* Uses the SQLExecutor interface so consumers can plug in any SQLite driver
|
|
9
|
+
* (expo-sqlite, better-sqlite3, etc.).
|
|
10
|
+
*
|
|
11
|
+
* Tables are created lazily on first operation via `ensureInit()`.
|
|
12
|
+
* The consumer owns the SQLExecutor lifecycle — `[Symbol.asyncDispose]` is a no-op.
|
|
13
|
+
*/
|
|
14
|
+
class SQLiteWalletRepository {
|
|
15
|
+
constructor(db, options) {
|
|
16
|
+
this.db = db;
|
|
17
|
+
this.version = 1;
|
|
18
|
+
this.initPromise = null;
|
|
19
|
+
this.prefix = sanitizePrefix(options?.prefix ?? "ark_");
|
|
20
|
+
this.tables = {
|
|
21
|
+
vtxos: `${this.prefix}vtxos`,
|
|
22
|
+
utxos: `${this.prefix}utxos`,
|
|
23
|
+
transactions: `${this.prefix}transactions`,
|
|
24
|
+
walletState: `${this.prefix}wallet_state`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
28
|
+
ensureInit() {
|
|
29
|
+
if (!this.initPromise) {
|
|
30
|
+
this.initPromise = this.init();
|
|
31
|
+
}
|
|
32
|
+
return this.initPromise;
|
|
33
|
+
}
|
|
34
|
+
async init() {
|
|
35
|
+
await this.db.run(`
|
|
36
|
+
CREATE TABLE IF NOT EXISTS ${this.tables.vtxos} (
|
|
37
|
+
txid TEXT NOT NULL,
|
|
38
|
+
vout INTEGER NOT NULL,
|
|
39
|
+
value INTEGER NOT NULL,
|
|
40
|
+
address TEXT NOT NULL,
|
|
41
|
+
tap_tree TEXT NOT NULL,
|
|
42
|
+
forfeit_cb TEXT NOT NULL,
|
|
43
|
+
forfeit_s TEXT NOT NULL,
|
|
44
|
+
intent_cb TEXT NOT NULL,
|
|
45
|
+
intent_s TEXT NOT NULL,
|
|
46
|
+
status_json TEXT NOT NULL,
|
|
47
|
+
virtual_status_json TEXT NOT NULL,
|
|
48
|
+
created_at TEXT NOT NULL,
|
|
49
|
+
is_unrolled INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
is_spent INTEGER,
|
|
51
|
+
spent_by TEXT,
|
|
52
|
+
settled_by TEXT,
|
|
53
|
+
ark_tx_id TEXT,
|
|
54
|
+
extra_witness_json TEXT,
|
|
55
|
+
assets_json TEXT,
|
|
56
|
+
PRIMARY KEY (txid, vout)
|
|
57
|
+
)
|
|
58
|
+
`);
|
|
59
|
+
await this.db.run(`
|
|
60
|
+
CREATE TABLE IF NOT EXISTS ${this.tables.utxos} (
|
|
61
|
+
txid TEXT NOT NULL,
|
|
62
|
+
vout INTEGER NOT NULL,
|
|
63
|
+
value INTEGER NOT NULL,
|
|
64
|
+
address TEXT NOT NULL,
|
|
65
|
+
tap_tree TEXT NOT NULL,
|
|
66
|
+
forfeit_cb TEXT NOT NULL,
|
|
67
|
+
forfeit_s TEXT NOT NULL,
|
|
68
|
+
intent_cb TEXT NOT NULL,
|
|
69
|
+
intent_s TEXT NOT NULL,
|
|
70
|
+
status_json TEXT NOT NULL,
|
|
71
|
+
extra_witness_json TEXT,
|
|
72
|
+
PRIMARY KEY (txid, vout)
|
|
73
|
+
)
|
|
74
|
+
`);
|
|
75
|
+
await this.db.run(`
|
|
76
|
+
CREATE TABLE IF NOT EXISTS ${this.tables.transactions} (
|
|
77
|
+
address TEXT NOT NULL,
|
|
78
|
+
boarding_txid TEXT NOT NULL,
|
|
79
|
+
commitment_txid TEXT NOT NULL,
|
|
80
|
+
ark_txid TEXT NOT NULL,
|
|
81
|
+
type TEXT NOT NULL,
|
|
82
|
+
amount INTEGER NOT NULL,
|
|
83
|
+
settled INTEGER NOT NULL DEFAULT 0,
|
|
84
|
+
created_at INTEGER NOT NULL,
|
|
85
|
+
assets_json TEXT,
|
|
86
|
+
PRIMARY KEY (address, boarding_txid, commitment_txid, ark_txid)
|
|
87
|
+
)
|
|
88
|
+
`);
|
|
89
|
+
await this.db.run(`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS ${this.tables.walletState} (
|
|
91
|
+
key TEXT PRIMARY KEY,
|
|
92
|
+
last_sync_time INTEGER,
|
|
93
|
+
settings_json TEXT
|
|
94
|
+
)
|
|
95
|
+
`);
|
|
96
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}vtxos_address ON ${this.tables.vtxos} (address)`);
|
|
97
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}utxos_address ON ${this.tables.utxos} (address)`);
|
|
98
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}transactions_address ON ${this.tables.transactions} (address)`);
|
|
99
|
+
}
|
|
100
|
+
async [Symbol.asyncDispose]() {
|
|
101
|
+
// no-op — consumer owns the SQLExecutor lifecycle
|
|
102
|
+
}
|
|
103
|
+
// ── Clear ──────────────────────────────────────────────────────────
|
|
104
|
+
async clear() {
|
|
105
|
+
await this.ensureInit();
|
|
106
|
+
await this.db.run(`DELETE FROM ${this.tables.vtxos}`);
|
|
107
|
+
await this.db.run(`DELETE FROM ${this.tables.utxos}`);
|
|
108
|
+
await this.db.run(`DELETE FROM ${this.tables.transactions}`);
|
|
109
|
+
await this.db.run(`DELETE FROM ${this.tables.walletState}`);
|
|
110
|
+
}
|
|
111
|
+
// ── VTXO management ────────────────────────────────────────────────
|
|
112
|
+
async getVtxos(address) {
|
|
113
|
+
await this.ensureInit();
|
|
114
|
+
const rows = await this.db.all(`SELECT * FROM ${this.tables.vtxos} WHERE address = ?`, [address]);
|
|
115
|
+
return rows.map(vtxoRowToDomain);
|
|
116
|
+
}
|
|
117
|
+
async saveVtxos(address, vtxos) {
|
|
118
|
+
await this.ensureInit();
|
|
119
|
+
for (const vtxo of vtxos) {
|
|
120
|
+
const s = (0, serialization_1.serializeVtxo)(vtxo);
|
|
121
|
+
await this.db.run(`INSERT OR REPLACE INTO ${this.tables.vtxos}
|
|
122
|
+
(txid, vout, value, address,
|
|
123
|
+
tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
|
|
124
|
+
status_json, virtual_status_json, created_at,
|
|
125
|
+
is_unrolled, is_spent, spent_by, settled_by, ark_tx_id,
|
|
126
|
+
extra_witness_json, assets_json)
|
|
127
|
+
VALUES (?, ?, ?, ?,
|
|
128
|
+
?, ?, ?, ?, ?,
|
|
129
|
+
?, ?, ?,
|
|
130
|
+
?, ?, ?, ?, ?,
|
|
131
|
+
?, ?)`, [
|
|
132
|
+
s.txid,
|
|
133
|
+
s.vout,
|
|
134
|
+
s.value,
|
|
135
|
+
address,
|
|
136
|
+
s.tapTree,
|
|
137
|
+
s.forfeitTapLeafScript.cb,
|
|
138
|
+
s.forfeitTapLeafScript.s,
|
|
139
|
+
s.intentTapLeafScript.cb,
|
|
140
|
+
s.intentTapLeafScript.s,
|
|
141
|
+
JSON.stringify(s.status),
|
|
142
|
+
JSON.stringify(s.virtualStatus),
|
|
143
|
+
typeof s.createdAt === "string"
|
|
144
|
+
? s.createdAt
|
|
145
|
+
: s.createdAt instanceof Date
|
|
146
|
+
? s.createdAt.toISOString()
|
|
147
|
+
: new Date(s.createdAt).toISOString(),
|
|
148
|
+
s.isUnrolled ? 1 : 0,
|
|
149
|
+
s.isSpent === undefined ? null : s.isSpent ? 1 : 0,
|
|
150
|
+
s.spentBy ?? null,
|
|
151
|
+
s.settledBy ?? null,
|
|
152
|
+
s.arkTxId ?? null,
|
|
153
|
+
s.extraWitness ? JSON.stringify(s.extraWitness) : null,
|
|
154
|
+
s.assets ? JSON.stringify(s.assets) : null,
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async deleteVtxos(address) {
|
|
159
|
+
await this.ensureInit();
|
|
160
|
+
await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE address = ?`, [address]);
|
|
161
|
+
}
|
|
162
|
+
// ── UTXO management ────────────────────────────────────────────────
|
|
163
|
+
async getUtxos(address) {
|
|
164
|
+
await this.ensureInit();
|
|
165
|
+
const rows = await this.db.all(`SELECT * FROM ${this.tables.utxos} WHERE address = ?`, [address]);
|
|
166
|
+
return rows.map(utxoRowToDomain);
|
|
167
|
+
}
|
|
168
|
+
async saveUtxos(address, utxos) {
|
|
169
|
+
await this.ensureInit();
|
|
170
|
+
for (const utxo of utxos) {
|
|
171
|
+
const s = (0, serialization_1.serializeUtxo)(utxo);
|
|
172
|
+
await this.db.run(`INSERT OR REPLACE INTO ${this.tables.utxos}
|
|
173
|
+
(txid, vout, value, address,
|
|
174
|
+
tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
|
|
175
|
+
status_json, extra_witness_json)
|
|
176
|
+
VALUES (?, ?, ?, ?,
|
|
177
|
+
?, ?, ?, ?, ?,
|
|
178
|
+
?, ?)`, [
|
|
179
|
+
s.txid,
|
|
180
|
+
s.vout,
|
|
181
|
+
s.value,
|
|
182
|
+
address,
|
|
183
|
+
s.tapTree,
|
|
184
|
+
s.forfeitTapLeafScript.cb,
|
|
185
|
+
s.forfeitTapLeafScript.s,
|
|
186
|
+
s.intentTapLeafScript.cb,
|
|
187
|
+
s.intentTapLeafScript.s,
|
|
188
|
+
JSON.stringify(s.status),
|
|
189
|
+
s.extraWitness ? JSON.stringify(s.extraWitness) : null,
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async deleteUtxos(address) {
|
|
194
|
+
await this.ensureInit();
|
|
195
|
+
await this.db.run(`DELETE FROM ${this.tables.utxos} WHERE address = ?`, [address]);
|
|
196
|
+
}
|
|
197
|
+
// ── Transaction history ────────────────────────────────────────────
|
|
198
|
+
async getTransactionHistory(address) {
|
|
199
|
+
await this.ensureInit();
|
|
200
|
+
const rows = await this.db.all(`SELECT * FROM ${this.tables.transactions} WHERE address = ? ORDER BY created_at ASC`, [address]);
|
|
201
|
+
return rows.map(txRowToDomain);
|
|
202
|
+
}
|
|
203
|
+
async saveTransactions(address, txs) {
|
|
204
|
+
await this.ensureInit();
|
|
205
|
+
for (const tx of txs) {
|
|
206
|
+
await this.db.run(`INSERT OR REPLACE INTO ${this.tables.transactions}
|
|
207
|
+
(address, boarding_txid, commitment_txid, ark_txid,
|
|
208
|
+
type, amount, settled, created_at, assets_json)
|
|
209
|
+
VALUES (?, ?, ?, ?,
|
|
210
|
+
?, ?, ?, ?, ?)`, [
|
|
211
|
+
address,
|
|
212
|
+
tx.key.boardingTxid,
|
|
213
|
+
tx.key.commitmentTxid,
|
|
214
|
+
tx.key.arkTxid,
|
|
215
|
+
tx.type,
|
|
216
|
+
tx.amount,
|
|
217
|
+
tx.settled ? 1 : 0,
|
|
218
|
+
tx.createdAt,
|
|
219
|
+
tx.assets ? JSON.stringify(tx.assets) : null,
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async deleteTransactions(address) {
|
|
224
|
+
await this.ensureInit();
|
|
225
|
+
await this.db.run(`DELETE FROM ${this.tables.transactions} WHERE address = ?`, [address]);
|
|
226
|
+
}
|
|
227
|
+
// ── Wallet state ───────────────────────────────────────────────────
|
|
228
|
+
async getWalletState() {
|
|
229
|
+
await this.ensureInit();
|
|
230
|
+
const row = await this.db.get(`SELECT * FROM ${this.tables.walletState} WHERE key = ?`, ["state"]);
|
|
231
|
+
if (!row)
|
|
232
|
+
return null;
|
|
233
|
+
const state = {};
|
|
234
|
+
if (row.last_sync_time !== null && row.last_sync_time !== undefined) {
|
|
235
|
+
state.lastSyncTime = row.last_sync_time;
|
|
236
|
+
}
|
|
237
|
+
if (row.settings_json) {
|
|
238
|
+
state.settings = JSON.parse(row.settings_json);
|
|
239
|
+
}
|
|
240
|
+
return state;
|
|
241
|
+
}
|
|
242
|
+
async saveWalletState(state) {
|
|
243
|
+
await this.ensureInit();
|
|
244
|
+
await this.db.run(`INSERT OR REPLACE INTO ${this.tables.walletState}
|
|
245
|
+
(key, last_sync_time, settings_json)
|
|
246
|
+
VALUES (?, ?, ?)`, [
|
|
247
|
+
"state",
|
|
248
|
+
state.lastSyncTime ?? null,
|
|
249
|
+
state.settings ? JSON.stringify(state.settings) : null,
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
exports.SQLiteWalletRepository = SQLiteWalletRepository;
|
|
254
|
+
const SAFE_PREFIX = /^[a-zA-Z0-9_]+$/;
|
|
255
|
+
function sanitizePrefix(prefix) {
|
|
256
|
+
if (!SAFE_PREFIX.test(prefix)) {
|
|
257
|
+
throw new Error(`Invalid table prefix "${prefix}": only letters, digits, and underscores are allowed`);
|
|
258
|
+
}
|
|
259
|
+
return prefix;
|
|
260
|
+
}
|
|
261
|
+
// ── Row → Domain converters ────────────────────────────────────────────
|
|
262
|
+
function vtxoRowToDomain(row) {
|
|
263
|
+
const serialized = {
|
|
264
|
+
txid: row.txid,
|
|
265
|
+
vout: row.vout,
|
|
266
|
+
value: row.value,
|
|
267
|
+
tapTree: row.tap_tree,
|
|
268
|
+
forfeitTapLeafScript: {
|
|
269
|
+
cb: row.forfeit_cb,
|
|
270
|
+
s: row.forfeit_s,
|
|
271
|
+
},
|
|
272
|
+
intentTapLeafScript: {
|
|
273
|
+
cb: row.intent_cb,
|
|
274
|
+
s: row.intent_s,
|
|
275
|
+
},
|
|
276
|
+
status: JSON.parse(row.status_json),
|
|
277
|
+
virtualStatus: JSON.parse(row.virtual_status_json),
|
|
278
|
+
createdAt: new Date(row.created_at),
|
|
279
|
+
isUnrolled: row.is_unrolled === 1,
|
|
280
|
+
isSpent: row.is_spent === null ? undefined : row.is_spent === 1,
|
|
281
|
+
spentBy: row.spent_by ?? undefined,
|
|
282
|
+
settledBy: row.settled_by ?? undefined,
|
|
283
|
+
arkTxId: row.ark_tx_id ?? undefined,
|
|
284
|
+
extraWitness: row.extra_witness_json
|
|
285
|
+
? JSON.parse(row.extra_witness_json)
|
|
286
|
+
: undefined,
|
|
287
|
+
assets: row.assets_json ? JSON.parse(row.assets_json) : undefined,
|
|
288
|
+
};
|
|
289
|
+
return (0, serialization_1.deserializeVtxo)(serialized);
|
|
290
|
+
}
|
|
291
|
+
function utxoRowToDomain(row) {
|
|
292
|
+
const serialized = {
|
|
293
|
+
txid: row.txid,
|
|
294
|
+
vout: row.vout,
|
|
295
|
+
value: row.value,
|
|
296
|
+
tapTree: row.tap_tree,
|
|
297
|
+
forfeitTapLeafScript: {
|
|
298
|
+
cb: row.forfeit_cb,
|
|
299
|
+
s: row.forfeit_s,
|
|
300
|
+
},
|
|
301
|
+
intentTapLeafScript: {
|
|
302
|
+
cb: row.intent_cb,
|
|
303
|
+
s: row.intent_s,
|
|
304
|
+
},
|
|
305
|
+
status: JSON.parse(row.status_json),
|
|
306
|
+
extraWitness: row.extra_witness_json
|
|
307
|
+
? JSON.parse(row.extra_witness_json)
|
|
308
|
+
: undefined,
|
|
309
|
+
};
|
|
310
|
+
return (0, serialization_1.deserializeUtxo)(serialized);
|
|
311
|
+
}
|
|
312
|
+
function txRowToDomain(row) {
|
|
313
|
+
const tx = {
|
|
314
|
+
key: {
|
|
315
|
+
boardingTxid: row.boarding_txid,
|
|
316
|
+
commitmentTxid: row.commitment_txid,
|
|
317
|
+
arkTxid: row.ark_txid,
|
|
318
|
+
},
|
|
319
|
+
type: row.type,
|
|
320
|
+
amount: row.amount,
|
|
321
|
+
settled: row.settled === 1,
|
|
322
|
+
createdAt: row.created_at,
|
|
323
|
+
};
|
|
324
|
+
if (row.assets_json) {
|
|
325
|
+
tx.assets = JSON.parse(row.assets_json);
|
|
326
|
+
}
|
|
327
|
+
return tx;
|
|
328
|
+
}
|
|
@@ -169,12 +169,20 @@ class ServiceWorkerReadonlyWallet {
|
|
|
169
169
|
// send a message and wait for a response
|
|
170
170
|
async sendMessage(request) {
|
|
171
171
|
return new Promise((resolve, reject) => {
|
|
172
|
+
const cleanup = () => {
|
|
173
|
+
clearTimeout(timeoutId);
|
|
174
|
+
navigator.serviceWorker.removeEventListener("message", messageHandler);
|
|
175
|
+
};
|
|
176
|
+
const timeoutId = setTimeout(() => {
|
|
177
|
+
cleanup();
|
|
178
|
+
reject(new Error(`Service worker message timed out (${request.type})`));
|
|
179
|
+
}, 30000);
|
|
172
180
|
const messageHandler = (event) => {
|
|
173
181
|
const response = event.data;
|
|
174
182
|
if (request.id !== response.id) {
|
|
175
183
|
return;
|
|
176
184
|
}
|
|
177
|
-
|
|
185
|
+
cleanup();
|
|
178
186
|
if (response.error) {
|
|
179
187
|
reject(response.error);
|
|
180
188
|
}
|
|
@@ -93,6 +93,13 @@ function isVtxoExpiringSoon(vtxo, thresholdMs // in milliseconds
|
|
|
93
93
|
const { batchExpiry } = vtxo.virtualStatus;
|
|
94
94
|
if (!batchExpiry)
|
|
95
95
|
return false; // it doesn't expire
|
|
96
|
+
// we use this as a workaround to avoid issue on regtest where expiry date is
|
|
97
|
+
// expressed in blockheight instead of timestamp. If expiry, as Date, is before 2025,
|
|
98
|
+
// then we admit it's too small to be a timestamp
|
|
99
|
+
// TODO: API should return the expiry unit
|
|
100
|
+
const expireAt = new Date(batchExpiry);
|
|
101
|
+
if (expireAt.getFullYear() < 2025)
|
|
102
|
+
return false;
|
|
96
103
|
const now = Date.now();
|
|
97
104
|
if (batchExpiry <= now)
|
|
98
105
|
return false; // already expired
|
|
@@ -107,8 +107,19 @@ class MessageBus {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
async waitForInit(config) {
|
|
110
|
-
if (this.initialized)
|
|
111
|
-
|
|
110
|
+
if (this.initialized) {
|
|
111
|
+
// Stop existing handlers before re-initializing.
|
|
112
|
+
// This handles the case where CLEAR was called, which nullifies
|
|
113
|
+
// handler state (readonlyWallet, etc.) without resetting the
|
|
114
|
+
// initialized flag. Without this, handlers never get start()
|
|
115
|
+
// called again and all messages fail with "not initialized".
|
|
116
|
+
//
|
|
117
|
+
// Clear the flag first so onMessage() rejects incoming messages
|
|
118
|
+
// during the stop/start window instead of routing them to
|
|
119
|
+
// half-reset handlers. Restored to true after start() completes.
|
|
120
|
+
this.initialized = false;
|
|
121
|
+
await Promise.all(Array.from(this.handlers.values()).map((h) => h.stop().catch(() => { })));
|
|
122
|
+
}
|
|
112
123
|
const services = await this.buildServicesFn(config);
|
|
113
124
|
// Start all handlers
|
|
114
125
|
for (const updater of this.handlers.values()) {
|
|
@@ -168,6 +179,15 @@ class MessageBus {
|
|
|
168
179
|
if (!this.initialized) {
|
|
169
180
|
if (this.debug)
|
|
170
181
|
console.warn("Event received before initialization, dropping", event.data);
|
|
182
|
+
// Send error response so the caller's promise rejects instead of
|
|
183
|
+
// hanging forever. This happens when the browser kills and restarts
|
|
184
|
+
// the service worker — the new instance has initialized=false and
|
|
185
|
+
// messages arrive before INITIALIZE_MESSAGE_BUS is re-sent.
|
|
186
|
+
event.source?.postMessage({
|
|
187
|
+
id,
|
|
188
|
+
tag: tag ?? "unknown",
|
|
189
|
+
error: new Error("MessageBus not initialized"),
|
|
190
|
+
});
|
|
171
191
|
return;
|
|
172
192
|
}
|
|
173
193
|
if (!id || !tag) {
|