webycash-sdk 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend-ffi.d.ts +48 -0
- package/dist/backend-ffi.js +269 -0
- package/dist/backend-wasm.d.ts +50 -0
- package/dist/backend-wasm.js +275 -0
- package/dist/backend.d.ts +56 -0
- package/dist/backend.js +1 -0
- package/dist/error.d.ts +10 -0
- package/dist/error.js +19 -0
- package/dist/index.d.ts +26 -21
- package/dist/index.js +45 -145
- package/dist/init.d.ts +6 -0
- package/dist/init.js +35 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +14 -0
- package/dist/wallet.d.ts +53 -0
- package/dist/wallet.js +108 -0
- package/libwebycash_sdk.dylib +0 -0
- package/libwebycash_sdk.so +0 -0
- package/package.json +20 -7
- package/wasm/package.json +31 -0
- package/wasm/webycash_sdk_wasm.d.ts +207 -0
- package/wasm/webycash_sdk_wasm.js +1308 -0
- package/wasm/webycash_sdk_wasm_bg.wasm +0 -0
- package/wasm/webycash_sdk_wasm_bg.wasm.d.ts +43 -0
- package/webycash_sdk.dll +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Backend, WalletHandle } from "./backend.js";
|
|
2
|
+
import type { CheckResult, NetworkMode, ParsedWebcash, RecoveryResult, WalletStats } from "./types.js";
|
|
3
|
+
export declare class FfiBackend implements Backend {
|
|
4
|
+
readonly name: "ffi";
|
|
5
|
+
static create(): FfiBackend;
|
|
6
|
+
walletOpen(dbPath: string, _network: NetworkMode, seed?: Uint8Array): Promise<WalletHandle>;
|
|
7
|
+
walletClose(handle: WalletHandle): void;
|
|
8
|
+
walletBalance(handle: WalletHandle): Promise<string>;
|
|
9
|
+
walletInsert(handle: WalletHandle, webcash: string): Promise<WalletHandle>;
|
|
10
|
+
walletPay(handle: WalletHandle, amount: string, memo: string): Promise<{
|
|
11
|
+
handle: WalletHandle;
|
|
12
|
+
paymentWebcash: string;
|
|
13
|
+
}>;
|
|
14
|
+
walletCheck(handle: WalletHandle): Promise<{
|
|
15
|
+
handle: WalletHandle;
|
|
16
|
+
result: CheckResult;
|
|
17
|
+
}>;
|
|
18
|
+
walletMerge(handle: WalletHandle, maxOutputs: number): Promise<{
|
|
19
|
+
handle: WalletHandle;
|
|
20
|
+
message: string;
|
|
21
|
+
}>;
|
|
22
|
+
walletRecover(handle: WalletHandle, masterSecretHex: string, gapLimit: number): Promise<{
|
|
23
|
+
handle: WalletHandle;
|
|
24
|
+
result: RecoveryResult;
|
|
25
|
+
}>;
|
|
26
|
+
walletRecoverFromWallet(handle: WalletHandle, gapLimit: number): Promise<{
|
|
27
|
+
handle: WalletHandle;
|
|
28
|
+
result: RecoveryResult;
|
|
29
|
+
}>;
|
|
30
|
+
walletStats(handle: WalletHandle): Promise<WalletStats>;
|
|
31
|
+
walletExportSnapshot(handle: WalletHandle): Promise<string>;
|
|
32
|
+
walletImportSnapshot(handle: WalletHandle, json: string): Promise<WalletHandle>;
|
|
33
|
+
walletListWebcash(handle: WalletHandle): Promise<string[]>;
|
|
34
|
+
walletMasterSecret(handle: WalletHandle): Promise<string>;
|
|
35
|
+
walletEncryptSeed(handle: WalletHandle, password: string): Promise<void>;
|
|
36
|
+
walletEncryptWithPassword(handle: WalletHandle, password: string): Promise<string>;
|
|
37
|
+
walletDecryptWithPassword(handle: WalletHandle, encryptedJson: string, password: string): Promise<WalletHandle>;
|
|
38
|
+
version(): string;
|
|
39
|
+
amountParse(s: string): bigint;
|
|
40
|
+
amountFormat(wats: bigint): string;
|
|
41
|
+
deriveSecret(masterSecretHex: string, chainCode: number, depth: number): string;
|
|
42
|
+
generateMasterSecret(): string;
|
|
43
|
+
sha256Hex(data: string): string;
|
|
44
|
+
secretToPublic(secret: string): string;
|
|
45
|
+
parseWebcash(s: string): ParsedWebcash;
|
|
46
|
+
formatWebcash(secret: string, amountWats: bigint): string;
|
|
47
|
+
formatPublicWebcash(hashHex: string, amountWats: bigint): string;
|
|
48
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { WebycashError } from "./error.js";
|
|
2
|
+
import { WebycashErrorCode } from "./types.js";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { accessSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const esmRequire = createRequire(import.meta.url);
|
|
12
|
+
const LIB_NAME = {
|
|
13
|
+
linux: "libwebycash_sdk.so",
|
|
14
|
+
darwin: "libwebycash_sdk.dylib",
|
|
15
|
+
win32: "webycash_sdk.dll",
|
|
16
|
+
};
|
|
17
|
+
function findLib() {
|
|
18
|
+
const name = LIB_NAME[os.platform()];
|
|
19
|
+
if (!name)
|
|
20
|
+
throw new Error(`Unsupported platform: ${os.platform()}`);
|
|
21
|
+
const local = path.join(__dirname, "..", name);
|
|
22
|
+
try {
|
|
23
|
+
accessSync(local);
|
|
24
|
+
return local;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* fall through */
|
|
28
|
+
}
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
function check(rc) {
|
|
32
|
+
if (rc !== 0) {
|
|
33
|
+
const msg = ffi.weby_last_error_message();
|
|
34
|
+
throw new WebycashError(rc, msg ?? `Error code ${rc}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function takeString(val) {
|
|
38
|
+
if (val == null)
|
|
39
|
+
return "";
|
|
40
|
+
return String(val);
|
|
41
|
+
}
|
|
42
|
+
/** Parse "Recovery completed! Webcash recovered: N, Total amount: X" */
|
|
43
|
+
function parseRecoveryResult(s) {
|
|
44
|
+
const countMatch = s.match(/recovered:\s*(\d+)/i);
|
|
45
|
+
const amountMatch = s.match(/amount:\s*([\d.]+)/i);
|
|
46
|
+
return {
|
|
47
|
+
recoveredCount: countMatch ? parseInt(countMatch[1], 10) : 0,
|
|
48
|
+
totalAmount: amountMatch ? BigInt(Math.round(parseFloat(amountMatch[1]) * 1e8)) : 0n,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function assertFfi(handle) {
|
|
52
|
+
if (handle._tag !== "ffi")
|
|
53
|
+
throw new Error("Expected FFI handle");
|
|
54
|
+
return handle.ptr;
|
|
55
|
+
}
|
|
56
|
+
// Lazy-load koffi — singleton to avoid duplicate type registration
|
|
57
|
+
let ffi = null;
|
|
58
|
+
function loadFfi() {
|
|
59
|
+
if (ffi)
|
|
60
|
+
return ffi;
|
|
61
|
+
const koffi = esmRequire("koffi");
|
|
62
|
+
const lib = koffi.load(findLib());
|
|
63
|
+
const WebyWalletPtr = koffi.pointer("WebyWallet", koffi.opaque());
|
|
64
|
+
const bindings = {
|
|
65
|
+
// Lifecycle
|
|
66
|
+
weby_wallet_open: lib.func("int32_t weby_wallet_open(const char*, _Out_ WebyWallet**)"),
|
|
67
|
+
weby_wallet_open_with_seed: lib.func("int32_t weby_wallet_open_with_seed(const char*, const uint8_t*, size_t, _Out_ WebyWallet**)"),
|
|
68
|
+
weby_wallet_free: lib.func("void weby_wallet_free(WebyWallet*)"),
|
|
69
|
+
// Operations
|
|
70
|
+
weby_wallet_balance: lib.func("int32_t weby_wallet_balance(const WebyWallet*, _Out_ char**)"),
|
|
71
|
+
weby_wallet_insert: lib.func("int32_t weby_wallet_insert(const WebyWallet*, const char*)"),
|
|
72
|
+
weby_wallet_pay: lib.func("int32_t weby_wallet_pay(const WebyWallet*, const char*, const char*, _Out_ char**)"),
|
|
73
|
+
weby_wallet_check: lib.func("int32_t weby_wallet_check(const WebyWallet*)"),
|
|
74
|
+
weby_wallet_merge: lib.func("int32_t weby_wallet_merge(const WebyWallet*, uint32_t, _Out_ char**)"),
|
|
75
|
+
weby_wallet_recover: lib.func("int32_t weby_wallet_recover(const WebyWallet*, const char*, uint32_t, _Out_ char**)"),
|
|
76
|
+
weby_wallet_recover_from_wallet: lib.func("int32_t weby_wallet_recover_from_wallet(const WebyWallet*, uint32_t, _Out_ char**)"),
|
|
77
|
+
// Inspection
|
|
78
|
+
weby_wallet_stats: lib.func("int32_t weby_wallet_stats(const WebyWallet*, _Out_ char**)"),
|
|
79
|
+
weby_wallet_export_snapshot: lib.func("int32_t weby_wallet_export_snapshot(const WebyWallet*, _Out_ char**)"),
|
|
80
|
+
weby_wallet_import_snapshot: lib.func("int32_t weby_wallet_import_snapshot(const WebyWallet*, const char*)"),
|
|
81
|
+
weby_wallet_list_webcash: lib.func("int32_t weby_wallet_list_webcash(const WebyWallet*, _Out_ char**)"),
|
|
82
|
+
weby_wallet_master_secret: lib.func("int32_t weby_wallet_master_secret(const WebyWallet*, _Out_ char**)"),
|
|
83
|
+
// Encryption
|
|
84
|
+
weby_wallet_encrypt_seed: lib.func("int32_t weby_wallet_encrypt_seed(const WebyWallet*, const char*)"),
|
|
85
|
+
weby_wallet_encrypt_with_password: lib.func("int32_t weby_wallet_encrypt_with_password(const WebyWallet*, const char*, _Out_ char**)"),
|
|
86
|
+
weby_wallet_decrypt_with_password: lib.func("int32_t weby_wallet_decrypt_with_password(const WebyWallet*, const char*, const char*)"),
|
|
87
|
+
// Utilities
|
|
88
|
+
weby_version: lib.func("const char* weby_version()"),
|
|
89
|
+
weby_last_error_message: lib.func("const char* weby_last_error_message()"),
|
|
90
|
+
weby_amount_parse: lib.func("int32_t weby_amount_parse(const char*, _Out_ int64_t*)"),
|
|
91
|
+
weby_amount_format: lib.func("int32_t weby_amount_format(int64_t, _Out_ char**)"),
|
|
92
|
+
weby_free_string: lib.func("void weby_free_string(char*)"),
|
|
93
|
+
WebyWalletPtr,
|
|
94
|
+
};
|
|
95
|
+
ffi = bindings;
|
|
96
|
+
return bindings;
|
|
97
|
+
}
|
|
98
|
+
export class FfiBackend {
|
|
99
|
+
name = "ffi";
|
|
100
|
+
static create() {
|
|
101
|
+
loadFfi();
|
|
102
|
+
return new FfiBackend();
|
|
103
|
+
}
|
|
104
|
+
// ── Lifecycle ──────────────────────────────────────────────
|
|
105
|
+
async walletOpen(dbPath, _network, seed) {
|
|
106
|
+
const out = [null];
|
|
107
|
+
if (seed) {
|
|
108
|
+
if (seed.length !== 32)
|
|
109
|
+
throw new WebycashError(WebycashErrorCode.InvalidInput, "seed must be 32 bytes");
|
|
110
|
+
check(ffi.weby_wallet_open_with_seed(dbPath, Buffer.from(seed), seed.length, out));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
check(ffi.weby_wallet_open(dbPath, out));
|
|
114
|
+
}
|
|
115
|
+
return { _tag: "ffi", ptr: out[0] };
|
|
116
|
+
}
|
|
117
|
+
walletClose(handle) {
|
|
118
|
+
const ptr = assertFfi(handle);
|
|
119
|
+
if (ptr)
|
|
120
|
+
ffi.weby_wallet_free(ptr);
|
|
121
|
+
}
|
|
122
|
+
// ── Operations ─────────────────────────────────────────────
|
|
123
|
+
async walletBalance(handle) {
|
|
124
|
+
const out = [null];
|
|
125
|
+
check(ffi.weby_wallet_balance(assertFfi(handle), out));
|
|
126
|
+
return takeString(out[0]);
|
|
127
|
+
}
|
|
128
|
+
async walletInsert(handle, webcash) {
|
|
129
|
+
check(ffi.weby_wallet_insert(assertFfi(handle), webcash));
|
|
130
|
+
return handle;
|
|
131
|
+
}
|
|
132
|
+
async walletPay(handle, amount, memo) {
|
|
133
|
+
const out = [null];
|
|
134
|
+
check(ffi.weby_wallet_pay(assertFfi(handle), amount, memo, out));
|
|
135
|
+
return { handle, paymentWebcash: takeString(out[0]) };
|
|
136
|
+
}
|
|
137
|
+
async walletCheck(handle) {
|
|
138
|
+
// FFI check() only returns success/failure — no detailed counts available via C ABI
|
|
139
|
+
check(ffi.weby_wallet_check(assertFfi(handle)));
|
|
140
|
+
// Re-query stats to get actual counts post-check
|
|
141
|
+
const stats = await this.walletStats(handle);
|
|
142
|
+
return { handle, result: { validCount: stats.unspentWebcash, spentCount: stats.spentWebcash } };
|
|
143
|
+
}
|
|
144
|
+
async walletMerge(handle, maxOutputs) {
|
|
145
|
+
const out = [null];
|
|
146
|
+
check(ffi.weby_wallet_merge(assertFfi(handle), maxOutputs, out));
|
|
147
|
+
return { handle, message: takeString(out[0]) };
|
|
148
|
+
}
|
|
149
|
+
async walletRecover(handle, masterSecretHex, gapLimit) {
|
|
150
|
+
const out = [null];
|
|
151
|
+
check(ffi.weby_wallet_recover(assertFfi(handle), masterSecretHex, gapLimit, out));
|
|
152
|
+
return { handle, result: parseRecoveryResult(takeString(out[0])) };
|
|
153
|
+
}
|
|
154
|
+
async walletRecoverFromWallet(handle, gapLimit) {
|
|
155
|
+
const out = [null];
|
|
156
|
+
check(ffi.weby_wallet_recover_from_wallet(assertFfi(handle), gapLimit, out));
|
|
157
|
+
return { handle, result: parseRecoveryResult(takeString(out[0])) };
|
|
158
|
+
}
|
|
159
|
+
// ── Inspection ─────────────────────────────────────────────
|
|
160
|
+
async walletStats(handle) {
|
|
161
|
+
const out = [null];
|
|
162
|
+
check(ffi.weby_wallet_stats(assertFfi(handle), out));
|
|
163
|
+
const raw = JSON.parse(takeString(out[0]));
|
|
164
|
+
return {
|
|
165
|
+
totalWebcash: raw.total_webcash,
|
|
166
|
+
unspentWebcash: raw.unspent_webcash,
|
|
167
|
+
spentWebcash: raw.spent_webcash,
|
|
168
|
+
totalBalance: raw.total_balance,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async walletExportSnapshot(handle) {
|
|
172
|
+
const out = [null];
|
|
173
|
+
check(ffi.weby_wallet_export_snapshot(assertFfi(handle), out));
|
|
174
|
+
return takeString(out[0]);
|
|
175
|
+
}
|
|
176
|
+
async walletImportSnapshot(handle, json) {
|
|
177
|
+
check(ffi.weby_wallet_import_snapshot(assertFfi(handle), json));
|
|
178
|
+
return handle;
|
|
179
|
+
}
|
|
180
|
+
async walletListWebcash(handle) {
|
|
181
|
+
const out = [null];
|
|
182
|
+
check(ffi.weby_wallet_list_webcash(assertFfi(handle), out));
|
|
183
|
+
return JSON.parse(takeString(out[0]));
|
|
184
|
+
}
|
|
185
|
+
async walletMasterSecret(handle) {
|
|
186
|
+
const out = [null];
|
|
187
|
+
check(ffi.weby_wallet_master_secret(assertFfi(handle), out));
|
|
188
|
+
return takeString(out[0]);
|
|
189
|
+
}
|
|
190
|
+
// ── Encryption ─────────────────────────────────────────────
|
|
191
|
+
async walletEncryptSeed(handle, password) {
|
|
192
|
+
check(ffi.weby_wallet_encrypt_seed(assertFfi(handle), password));
|
|
193
|
+
}
|
|
194
|
+
async walletEncryptWithPassword(handle, password) {
|
|
195
|
+
const out = [null];
|
|
196
|
+
check(ffi.weby_wallet_encrypt_with_password(assertFfi(handle), password, out));
|
|
197
|
+
return takeString(out[0]);
|
|
198
|
+
}
|
|
199
|
+
async walletDecryptWithPassword(handle, encryptedJson, password) {
|
|
200
|
+
check(ffi.weby_wallet_decrypt_with_password(assertFfi(handle), encryptedJson, password));
|
|
201
|
+
return handle;
|
|
202
|
+
}
|
|
203
|
+
// ── Utilities ──────────────────────────────────────────────
|
|
204
|
+
version() {
|
|
205
|
+
return ffi.weby_version();
|
|
206
|
+
}
|
|
207
|
+
amountParse(s) {
|
|
208
|
+
const out = [0];
|
|
209
|
+
check(ffi.weby_amount_parse(s, out));
|
|
210
|
+
return BigInt(out[0]);
|
|
211
|
+
}
|
|
212
|
+
amountFormat(wats) {
|
|
213
|
+
const out = [null];
|
|
214
|
+
check(ffi.weby_amount_format(Number(wats), out));
|
|
215
|
+
return takeString(out[0]);
|
|
216
|
+
}
|
|
217
|
+
// ── Crypto (pure Node.js — no FFI needed) ──────────────────
|
|
218
|
+
deriveSecret(masterSecretHex, chainCode, depth) {
|
|
219
|
+
const masterBytes = Buffer.from(masterSecretHex, "hex");
|
|
220
|
+
if (masterBytes.length !== 32)
|
|
221
|
+
throw new WebycashError(WebycashErrorCode.InvalidInput, "master secret must be 32 bytes (64 hex chars)");
|
|
222
|
+
const tag = createHash("sha256").update("webcashwalletv1").digest();
|
|
223
|
+
const h = createHash("sha256");
|
|
224
|
+
h.update(tag);
|
|
225
|
+
h.update(tag);
|
|
226
|
+
h.update(masterBytes);
|
|
227
|
+
const chainBuf = Buffer.alloc(8);
|
|
228
|
+
chainBuf.writeBigUInt64BE(BigInt(chainCode));
|
|
229
|
+
h.update(chainBuf);
|
|
230
|
+
const depthBuf = Buffer.alloc(8);
|
|
231
|
+
depthBuf.writeBigUInt64BE(BigInt(depth));
|
|
232
|
+
h.update(depthBuf);
|
|
233
|
+
return h.digest("hex");
|
|
234
|
+
}
|
|
235
|
+
generateMasterSecret() {
|
|
236
|
+
return randomBytes(32).toString("hex");
|
|
237
|
+
}
|
|
238
|
+
sha256Hex(data) {
|
|
239
|
+
return createHash("sha256").update(data, "ascii").digest("hex");
|
|
240
|
+
}
|
|
241
|
+
secretToPublic(secret) {
|
|
242
|
+
return this.sha256Hex(secret);
|
|
243
|
+
}
|
|
244
|
+
// ── Webcash (pure string ops) ──────────────────────────────
|
|
245
|
+
parseWebcash(s) {
|
|
246
|
+
const trimmed = s.trim();
|
|
247
|
+
if (!trimmed.startsWith("e"))
|
|
248
|
+
throw new WebycashError(WebycashErrorCode.InvalidInput, "webcash must start with 'e'");
|
|
249
|
+
const parts = trimmed.slice(1).split(":");
|
|
250
|
+
if (parts.length < 3 || parts[1] !== "secret")
|
|
251
|
+
throw new WebycashError(WebycashErrorCode.InvalidInput, "invalid webcash format");
|
|
252
|
+
const amountWats = this.amountParse(parts[0]);
|
|
253
|
+
const secret = parts.slice(2).join(":");
|
|
254
|
+
if (secret.length !== 64)
|
|
255
|
+
throw new WebycashError(WebycashErrorCode.InvalidInput, "secret must be 64 hex characters");
|
|
256
|
+
return {
|
|
257
|
+
secret,
|
|
258
|
+
amountWats,
|
|
259
|
+
amountDisplay: this.amountFormat(amountWats),
|
|
260
|
+
publicHash: this.secretToPublic(secret),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
formatWebcash(secret, amountWats) {
|
|
264
|
+
return `e${this.amountFormat(amountWats)}:secret:${secret}`;
|
|
265
|
+
}
|
|
266
|
+
formatPublicWebcash(hashHex, amountWats) {
|
|
267
|
+
return `e${this.amountFormat(amountWats)}:public:${hashHex}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Backend, WalletHandle } from "./backend.js";
|
|
2
|
+
import type { CheckResult, NetworkMode, ParsedWebcash, RecoveryResult, WalletStats } from "./types.js";
|
|
3
|
+
export declare class WasmBackend implements Backend {
|
|
4
|
+
readonly name: "wasm";
|
|
5
|
+
private wasm;
|
|
6
|
+
private constructor();
|
|
7
|
+
static create(): Promise<WasmBackend>;
|
|
8
|
+
walletOpen(_path: string, network: NetworkMode, seed?: Uint8Array): Promise<WalletHandle>;
|
|
9
|
+
walletClose(_handle: WalletHandle): void;
|
|
10
|
+
walletBalance(handle: WalletHandle): Promise<string>;
|
|
11
|
+
walletInsert(handle: WalletHandle, webcash: string): Promise<WalletHandle>;
|
|
12
|
+
walletPay(handle: WalletHandle, amount: string, _memo: string): Promise<{
|
|
13
|
+
handle: WalletHandle;
|
|
14
|
+
paymentWebcash: string;
|
|
15
|
+
}>;
|
|
16
|
+
walletCheck(handle: WalletHandle): Promise<{
|
|
17
|
+
handle: WalletHandle;
|
|
18
|
+
result: CheckResult;
|
|
19
|
+
}>;
|
|
20
|
+
walletMerge(handle: WalletHandle, maxOutputs: number): Promise<{
|
|
21
|
+
handle: WalletHandle;
|
|
22
|
+
message: string;
|
|
23
|
+
}>;
|
|
24
|
+
walletRecover(handle: WalletHandle, masterSecretHex: string, gapLimit: number): Promise<{
|
|
25
|
+
handle: WalletHandle;
|
|
26
|
+
result: RecoveryResult;
|
|
27
|
+
}>;
|
|
28
|
+
walletRecoverFromWallet(handle: WalletHandle, gapLimit: number): Promise<{
|
|
29
|
+
handle: WalletHandle;
|
|
30
|
+
result: RecoveryResult;
|
|
31
|
+
}>;
|
|
32
|
+
walletStats(handle: WalletHandle): Promise<WalletStats>;
|
|
33
|
+
walletExportSnapshot(handle: WalletHandle): Promise<string>;
|
|
34
|
+
walletImportSnapshot(handle: WalletHandle, json: string): Promise<WalletHandle>;
|
|
35
|
+
walletListWebcash(handle: WalletHandle): Promise<string[]>;
|
|
36
|
+
walletMasterSecret(handle: WalletHandle): Promise<string>;
|
|
37
|
+
walletEncryptSeed(_handle: WalletHandle, _password: string): Promise<void>;
|
|
38
|
+
walletEncryptWithPassword(handle: WalletHandle, password: string): Promise<string>;
|
|
39
|
+
walletDecryptWithPassword(handle: WalletHandle, encryptedJson: string, password: string): Promise<WalletHandle>;
|
|
40
|
+
version(): string;
|
|
41
|
+
amountParse(s: string): bigint;
|
|
42
|
+
amountFormat(wats: bigint): string;
|
|
43
|
+
deriveSecret(masterSecretHex: string, chainCode: number, depth: number): string;
|
|
44
|
+
generateMasterSecret(): string;
|
|
45
|
+
sha256Hex(data: string): string;
|
|
46
|
+
secretToPublic(secret: string): string;
|
|
47
|
+
parseWebcash(s: string): ParsedWebcash;
|
|
48
|
+
formatWebcash(secret: string, amountWats: bigint): string;
|
|
49
|
+
formatPublicWebcash(hashHex: string, amountWats: bigint): string;
|
|
50
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { WebycashError } from "./error.js";
|
|
2
|
+
import { WebycashErrorCode } from "./types.js";
|
|
3
|
+
function assertWasm(handle) {
|
|
4
|
+
if (handle._tag !== "wasm")
|
|
5
|
+
throw new Error("Expected WASM handle");
|
|
6
|
+
return handle;
|
|
7
|
+
}
|
|
8
|
+
function wrapError(err) {
|
|
9
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10
|
+
throw new WebycashError(WebycashErrorCode.Unknown, msg);
|
|
11
|
+
}
|
|
12
|
+
export class WasmBackend {
|
|
13
|
+
name = "wasm";
|
|
14
|
+
wasm;
|
|
15
|
+
constructor(wasm) {
|
|
16
|
+
this.wasm = wasm;
|
|
17
|
+
}
|
|
18
|
+
static async create() {
|
|
19
|
+
// Dynamic import for the WASM module
|
|
20
|
+
let wasm;
|
|
21
|
+
try {
|
|
22
|
+
wasm = await import("../wasm/webycash_sdk_wasm.js");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new WebycashError(WebycashErrorCode.NotSupported, "WASM module not found. Ensure the WASM package is built (scripts/build-wasm.sh)");
|
|
26
|
+
}
|
|
27
|
+
// Initialize WASM (loads the .wasm binary)
|
|
28
|
+
if (typeof wasm.default === "function") {
|
|
29
|
+
// In Node.js, read the .wasm file from disk
|
|
30
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
31
|
+
const { readFile } = await import("node:fs/promises");
|
|
32
|
+
const { fileURLToPath } = await import("node:url");
|
|
33
|
+
const path = await import("node:path");
|
|
34
|
+
const wasmDir = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const wasmPath = path.join(wasmDir, "..", "wasm", "webycash_sdk_wasm_bg.wasm");
|
|
36
|
+
const wasmBytes = await readFile(wasmPath);
|
|
37
|
+
await wasm.default(wasmBytes);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Browser — let wasm-bindgen handle fetch
|
|
41
|
+
await wasm.default();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return new WasmBackend(wasm);
|
|
45
|
+
}
|
|
46
|
+
// ── Lifecycle ──────────────────────────────────────────────
|
|
47
|
+
async walletOpen(_path, network, seed) {
|
|
48
|
+
try {
|
|
49
|
+
let stateJson;
|
|
50
|
+
if (seed) {
|
|
51
|
+
if (seed.length !== 32)
|
|
52
|
+
throw new WebycashError(WebycashErrorCode.InvalidInput, "seed must be 32 bytes");
|
|
53
|
+
const hex = Array.from(seed, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
54
|
+
stateJson = await this.wasm.wallet_create_with_secret(network, hex);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
stateJson = this.wasm.wallet_create(network);
|
|
58
|
+
}
|
|
59
|
+
return { _tag: "wasm", stateJson, network };
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
wrapError(err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
walletClose(_handle) {
|
|
66
|
+
// WASM wallets are GC'd — no-op
|
|
67
|
+
}
|
|
68
|
+
// ── Operations ─────────────────────────────────────────────
|
|
69
|
+
async walletBalance(handle) {
|
|
70
|
+
try {
|
|
71
|
+
const { stateJson, network } = assertWasm(handle);
|
|
72
|
+
const wats = this.wasm.wallet_balance(stateJson, network);
|
|
73
|
+
return this.wasm.format_amount(wats);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
wrapError(err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async walletInsert(handle, webcash) {
|
|
80
|
+
try {
|
|
81
|
+
const { stateJson, network } = assertWasm(handle);
|
|
82
|
+
const newState = await this.wasm.wallet_insert(stateJson, network, webcash);
|
|
83
|
+
return { _tag: "wasm", stateJson: newState, network };
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
wrapError(err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async walletPay(handle, amount, _memo) {
|
|
90
|
+
try {
|
|
91
|
+
const { stateJson, network } = assertWasm(handle);
|
|
92
|
+
const amountWats = this.wasm.parse_amount(amount);
|
|
93
|
+
const resultJson = await this.wasm.wallet_pay(stateJson, network, amountWats);
|
|
94
|
+
const result = JSON.parse(resultJson);
|
|
95
|
+
return {
|
|
96
|
+
handle: { _tag: "wasm", stateJson: result.state, network },
|
|
97
|
+
paymentWebcash: result.payment_webcash,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
wrapError(err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async walletCheck(handle) {
|
|
105
|
+
try {
|
|
106
|
+
const { stateJson, network } = assertWasm(handle);
|
|
107
|
+
const resultJson = await this.wasm.wallet_check(stateJson, network);
|
|
108
|
+
const result = JSON.parse(resultJson);
|
|
109
|
+
return {
|
|
110
|
+
handle: { _tag: "wasm", stateJson: result.state, network },
|
|
111
|
+
result: { validCount: result.valid_count, spentCount: result.spent_count },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
wrapError(err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async walletMerge(handle, maxOutputs) {
|
|
119
|
+
try {
|
|
120
|
+
const { stateJson, network } = assertWasm(handle);
|
|
121
|
+
const resultJson = await this.wasm.wallet_merge(stateJson, network, maxOutputs);
|
|
122
|
+
const result = JSON.parse(resultJson);
|
|
123
|
+
return {
|
|
124
|
+
handle: { _tag: "wasm", stateJson: result.state, network },
|
|
125
|
+
message: result.message,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
wrapError(err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async walletRecover(handle, masterSecretHex, gapLimit) {
|
|
133
|
+
try {
|
|
134
|
+
const { stateJson, network } = assertWasm(handle);
|
|
135
|
+
const resultJson = await this.wasm.wallet_recover(stateJson, network, masterSecretHex, gapLimit);
|
|
136
|
+
const result = JSON.parse(resultJson);
|
|
137
|
+
return {
|
|
138
|
+
handle: { _tag: "wasm", stateJson: result.state, network },
|
|
139
|
+
result: { recoveredCount: result.recovered_count, totalAmount: BigInt(result.total_amount) },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
wrapError(err);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async walletRecoverFromWallet(handle, gapLimit) {
|
|
147
|
+
try {
|
|
148
|
+
const { stateJson, network } = assertWasm(handle);
|
|
149
|
+
const resultJson = await this.wasm.wallet_recover_from_wallet(stateJson, network, gapLimit);
|
|
150
|
+
const result = JSON.parse(resultJson);
|
|
151
|
+
return {
|
|
152
|
+
handle: { _tag: "wasm", stateJson: result.state, network },
|
|
153
|
+
result: { recoveredCount: result.recovered_count, totalAmount: BigInt(result.total_amount) },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
wrapError(err);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ── Inspection ─────────────────────────────────────────────
|
|
161
|
+
async walletStats(handle) {
|
|
162
|
+
try {
|
|
163
|
+
const { stateJson, network } = assertWasm(handle);
|
|
164
|
+
const raw = JSON.parse(this.wasm.wallet_stats(stateJson, network));
|
|
165
|
+
return {
|
|
166
|
+
totalWebcash: raw.total_webcash,
|
|
167
|
+
unspentWebcash: raw.unspent_webcash,
|
|
168
|
+
spentWebcash: raw.spent_webcash,
|
|
169
|
+
totalBalance: this.wasm.format_amount(BigInt(raw.total_balance)),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
wrapError(err);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async walletExportSnapshot(handle) {
|
|
177
|
+
try {
|
|
178
|
+
const { stateJson, network } = assertWasm(handle);
|
|
179
|
+
return this.wasm.wallet_export_snapshot(stateJson, network);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
wrapError(err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async walletImportSnapshot(handle, json) {
|
|
186
|
+
try {
|
|
187
|
+
const { network } = assertWasm(handle);
|
|
188
|
+
const newState = this.wasm.wallet_import_snapshot(json, network);
|
|
189
|
+
return { _tag: "wasm", stateJson: newState, network };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
wrapError(err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async walletListWebcash(handle) {
|
|
196
|
+
try {
|
|
197
|
+
const { stateJson, network } = assertWasm(handle);
|
|
198
|
+
return JSON.parse(this.wasm.wallet_list_webcash(stateJson, network));
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
wrapError(err);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async walletMasterSecret(handle) {
|
|
205
|
+
try {
|
|
206
|
+
const { stateJson, network } = assertWasm(handle);
|
|
207
|
+
return this.wasm.wallet_master_secret(stateJson, network);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
wrapError(err);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ── Encryption ─────────────────────────────────────────────
|
|
214
|
+
async walletEncryptSeed(_handle, _password) {
|
|
215
|
+
throw new WebycashError(WebycashErrorCode.NotSupported, "encryptSeed is only available with the FFI backend");
|
|
216
|
+
}
|
|
217
|
+
async walletEncryptWithPassword(handle, password) {
|
|
218
|
+
try {
|
|
219
|
+
const { stateJson, network } = assertWasm(handle);
|
|
220
|
+
return this.wasm.wallet_encrypt_with_password(stateJson, network, password);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
wrapError(err);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async walletDecryptWithPassword(handle, encryptedJson, password) {
|
|
227
|
+
try {
|
|
228
|
+
const { network } = assertWasm(handle);
|
|
229
|
+
const newState = this.wasm.wallet_decrypt_with_password(encryptedJson, password, network);
|
|
230
|
+
return { _tag: "wasm", stateJson: newState, network };
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
wrapError(err);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// ── Utilities ──────────────────────────────────────────────
|
|
237
|
+
version() {
|
|
238
|
+
return this.wasm.sdk_version();
|
|
239
|
+
}
|
|
240
|
+
amountParse(s) {
|
|
241
|
+
return this.wasm.parse_amount(s);
|
|
242
|
+
}
|
|
243
|
+
amountFormat(wats) {
|
|
244
|
+
return this.wasm.format_amount(wats);
|
|
245
|
+
}
|
|
246
|
+
// ── Crypto ─────────────────────────────────────────────────
|
|
247
|
+
deriveSecret(masterSecretHex, chainCode, depth) {
|
|
248
|
+
return this.wasm.derive_secret(masterSecretHex, chainCode, BigInt(depth));
|
|
249
|
+
}
|
|
250
|
+
generateMasterSecret() {
|
|
251
|
+
return this.wasm.generate_master_secret();
|
|
252
|
+
}
|
|
253
|
+
sha256Hex(data) {
|
|
254
|
+
return this.wasm.sha256_hex(data);
|
|
255
|
+
}
|
|
256
|
+
secretToPublic(secret) {
|
|
257
|
+
return this.wasm.secret_to_public(secret);
|
|
258
|
+
}
|
|
259
|
+
// ── Webcash ────────────────────────────────────────────────
|
|
260
|
+
parseWebcash(s) {
|
|
261
|
+
const raw = this.wasm.parse_webcash(s);
|
|
262
|
+
return {
|
|
263
|
+
secret: raw.secret,
|
|
264
|
+
amountWats: BigInt(raw.amount_wats),
|
|
265
|
+
amountDisplay: raw.amount_display,
|
|
266
|
+
publicHash: raw.public_hash,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
formatWebcash(secret, amountWats) {
|
|
270
|
+
return this.wasm.format_webcash(secret, amountWats);
|
|
271
|
+
}
|
|
272
|
+
formatPublicWebcash(hashHex, amountWats) {
|
|
273
|
+
return this.wasm.format_public_webcash(hashHex, amountWats);
|
|
274
|
+
}
|
|
275
|
+
}
|