@zcash-app/sdk 0.0.0-stage → 0.1.0-alpha.0
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/LICENSE +201 -0
- package/dist/client.d.ts +101 -0
- package/dist/client.js +247 -0
- package/dist/consensus.d.ts +20 -0
- package/dist/consensus.js +97 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +20 -0
- package/dist/dashboard-wallet.d.ts +36 -0
- package/dist/dashboard-wallet.js +178 -0
- package/dist/decrypted-tx.d.ts +26 -0
- package/dist/decrypted-tx.js +105 -0
- package/dist/detection.d.ts +20 -0
- package/dist/detection.js +252 -0
- package/dist/dev.d.ts +28 -0
- package/dist/dev.js +202 -0
- package/dist/explorer.d.ts +19 -0
- package/dist/explorer.js +43 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +33 -0
- package/dist/jsonrpc.d.ts +65 -0
- package/dist/jsonrpc.js +89 -0
- package/dist/keys.d.ts +4 -0
- package/dist/keys.js +81 -0
- package/dist/payment.d.ts +12 -0
- package/dist/payment.js +48 -0
- package/dist/send.d.ts +66 -0
- package/dist/send.js +301 -0
- package/dist/sync.d.ts +62 -0
- package/dist/sync.js +327 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +2 -0
- package/package.json +43 -3
- package/proto/compact_formats.proto +136 -0
- package/proto/service.proto +331 -0
- package/README.md +0 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KNOWN_TEST_MNEMONICS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Shared test fixture mnemonics (BIP-39 english).
|
|
6
|
+
* Publicly known seeds used exclusively for development and automated testing.
|
|
7
|
+
* Strictly prohibited on mainnet.
|
|
8
|
+
*/
|
|
9
|
+
exports.KNOWN_TEST_MNEMONICS = [
|
|
10
|
+
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
|
11
|
+
'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong',
|
|
12
|
+
'legal winner thank year wave sausage worth useful legal winner thank yellow',
|
|
13
|
+
'coast same leader clip mail imitate place never place disease gold endless',
|
|
14
|
+
'believe ketchup guitar sustain cushion hat napkin enforce require skull paper mail',
|
|
15
|
+
'mind mix pipe average diary crisp slam purse cram fiction depth sport',
|
|
16
|
+
'flat skate furnace copper burst deposit opinion marine napkin canal truth sick',
|
|
17
|
+
'pet inmate guard tiny okay damage tornado load addict insect alone merry',
|
|
18
|
+
'inflict miracle moment wish adult cry author disagree flip large total bronze',
|
|
19
|
+
'suggest travel flower pigeon digital dirt sudden shrimp sort urban miracle photo',
|
|
20
|
+
];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { WalletHandle } from '@zcash-app/core';
|
|
2
|
+
import { ZcashClient } from './client.js';
|
|
3
|
+
import { SyncOptions } from './sync.js';
|
|
4
|
+
/**
|
|
5
|
+
* SECURITY NOTICE:
|
|
6
|
+
* Memos and counterparties are stored in plaintext in the SQLite wallet database.
|
|
7
|
+
* To protect user privacy, directory permissions are strictly enforced to 0700
|
|
8
|
+
* and database files to 0600.
|
|
9
|
+
*/
|
|
10
|
+
export interface DashboardSyncStatus {
|
|
11
|
+
status: 'idle' | 'syncing' | 'synced' | 'error';
|
|
12
|
+
scanned: number;
|
|
13
|
+
target: number;
|
|
14
|
+
notesFound: number;
|
|
15
|
+
pendingFetches: number;
|
|
16
|
+
failedFetches: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
lastSyncedAt?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function clearDashboardWalletCache(): void;
|
|
21
|
+
export declare function getDashboardWalletDir(): string;
|
|
22
|
+
export declare function getDashboardWalletPath(network: string, accountId: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Opens or retrieves a cached WalletHandle for the given (network, account)
|
|
25
|
+
* persisted under ~/.zcash-app/dashboard/ with strict 0700 dir and 0600 file permissions.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getDashboardWallet(network: string, accountId: string, ufvk: string, birthday?: number): WalletHandle;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the current background sync status immediately without blocking.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getDashboardSyncStatus(network: string, accountId: string, wallet?: WalletHandle): DashboardSyncStatus;
|
|
32
|
+
/**
|
|
33
|
+
* Initiates an asynchronous background synchronization job if one is not already running.
|
|
34
|
+
* Returns immediately without blocking (< 50ms) so no API endpoint blocks.
|
|
35
|
+
*/
|
|
36
|
+
export declare function startDashboardSync(client: ZcashClient, wallet: WalletHandle, options?: SyncOptions): DashboardSyncStatus;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.clearDashboardWalletCache = clearDashboardWalletCache;
|
|
7
|
+
exports.getDashboardWalletDir = getDashboardWalletDir;
|
|
8
|
+
exports.getDashboardWalletPath = getDashboardWalletPath;
|
|
9
|
+
exports.getDashboardWallet = getDashboardWallet;
|
|
10
|
+
exports.getDashboardSyncStatus = getDashboardSyncStatus;
|
|
11
|
+
exports.startDashboardSync = startDashboardSync;
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const core_1 = require("@zcash-app/core");
|
|
16
|
+
const sync_js_1 = require("./sync.js");
|
|
17
|
+
const walletCache = new Map();
|
|
18
|
+
const syncStatusMap = new Map();
|
|
19
|
+
const syncPromiseMap = new Map();
|
|
20
|
+
function clearDashboardWalletCache() {
|
|
21
|
+
walletCache.clear();
|
|
22
|
+
syncStatusMap.clear();
|
|
23
|
+
syncPromiseMap.clear();
|
|
24
|
+
}
|
|
25
|
+
function getDashboardWalletDir() {
|
|
26
|
+
const baseHome = process.env.ZCASH_APP_HOME || os_1.default.homedir();
|
|
27
|
+
const dir = path_1.default.join(baseHome, '.zcash-app', 'dashboard');
|
|
28
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
29
|
+
fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
fs_1.default.chmodSync(dir, 0o700);
|
|
33
|
+
}
|
|
34
|
+
catch (_) { }
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
function getDashboardWalletPath(network, accountId) {
|
|
38
|
+
const dir = getDashboardWalletDir();
|
|
39
|
+
const safeNetwork = network.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
40
|
+
const safeAccount = accountId.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
41
|
+
return path_1.default.join(dir, `${safeNetwork}_${safeAccount}.db`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Opens or retrieves a cached WalletHandle for the given (network, account)
|
|
45
|
+
* persisted under ~/.zcash-app/dashboard/ with strict 0700 dir and 0600 file permissions.
|
|
46
|
+
*/
|
|
47
|
+
function getDashboardWallet(network, accountId, ufvk, birthday) {
|
|
48
|
+
const cacheKey = `${network.toLowerCase()}:${accountId.toLowerCase()}`;
|
|
49
|
+
const existing = walletCache.get(cacheKey);
|
|
50
|
+
if (existing) {
|
|
51
|
+
return existing;
|
|
52
|
+
}
|
|
53
|
+
const dbPath = getDashboardWalletPath(network, accountId);
|
|
54
|
+
const wallet = core_1.WalletHandle.open(dbPath, network, ufvk, accountId, birthday);
|
|
55
|
+
// Enforce 0600 on the database file
|
|
56
|
+
if (fs_1.default.existsSync(dbPath)) {
|
|
57
|
+
try {
|
|
58
|
+
fs_1.default.chmodSync(dbPath, 0o600);
|
|
59
|
+
}
|
|
60
|
+
catch (_) { }
|
|
61
|
+
}
|
|
62
|
+
walletCache.set(cacheKey, wallet);
|
|
63
|
+
return wallet;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns the current background sync status immediately without blocking.
|
|
67
|
+
*/
|
|
68
|
+
function getDashboardSyncStatus(network, accountId, wallet) {
|
|
69
|
+
const key = `${network.toLowerCase()}:${accountId.toLowerCase()}`;
|
|
70
|
+
let status = syncStatusMap.get(key);
|
|
71
|
+
if (!status) {
|
|
72
|
+
let scanned = 0;
|
|
73
|
+
let notesFound = 0;
|
|
74
|
+
let pendingFetches = 0;
|
|
75
|
+
let failedFetches = 0;
|
|
76
|
+
if (wallet) {
|
|
77
|
+
try {
|
|
78
|
+
const summary = wallet.getWalletSummary();
|
|
79
|
+
scanned = summary.lastScannedHeight;
|
|
80
|
+
notesFound = summary.unspentNotesCount;
|
|
81
|
+
const fetchCounts = wallet.getTxFetchCounts();
|
|
82
|
+
pendingFetches = fetchCounts.pendingCount;
|
|
83
|
+
failedFetches = fetchCounts.failedCount;
|
|
84
|
+
}
|
|
85
|
+
catch (_) { }
|
|
86
|
+
}
|
|
87
|
+
status = {
|
|
88
|
+
status: 'idle',
|
|
89
|
+
scanned,
|
|
90
|
+
target: scanned,
|
|
91
|
+
notesFound,
|
|
92
|
+
pendingFetches,
|
|
93
|
+
failedFetches,
|
|
94
|
+
};
|
|
95
|
+
syncStatusMap.set(key, status);
|
|
96
|
+
}
|
|
97
|
+
else if (wallet && status.status !== 'syncing') {
|
|
98
|
+
// Refresh counts from wallet if idle/synced/error
|
|
99
|
+
try {
|
|
100
|
+
const summary = wallet.getWalletSummary();
|
|
101
|
+
status.scanned = summary.lastScannedHeight;
|
|
102
|
+
status.notesFound = summary.unspentNotesCount;
|
|
103
|
+
const fetchCounts = wallet.getTxFetchCounts();
|
|
104
|
+
status.pendingFetches = fetchCounts.pendingCount;
|
|
105
|
+
status.failedFetches = fetchCounts.failedCount;
|
|
106
|
+
}
|
|
107
|
+
catch (_) { }
|
|
108
|
+
}
|
|
109
|
+
return { ...status };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Initiates an asynchronous background synchronization job if one is not already running.
|
|
113
|
+
* Returns immediately without blocking (< 50ms) so no API endpoint blocks.
|
|
114
|
+
*/
|
|
115
|
+
function startDashboardSync(client, wallet, options = {}) {
|
|
116
|
+
const network = options.network ?? wallet.network ?? client.config?.network ?? 'testnet';
|
|
117
|
+
const accountId = options.accountId ?? wallet.accountId ?? 'account0';
|
|
118
|
+
const key = `${network.toLowerCase()}:${accountId.toLowerCase()}`;
|
|
119
|
+
// If already syncing, return current status immediately
|
|
120
|
+
if (syncPromiseMap.has(key)) {
|
|
121
|
+
return getDashboardSyncStatus(network, accountId, wallet);
|
|
122
|
+
}
|
|
123
|
+
const currentSummary = wallet.getWalletSummary();
|
|
124
|
+
const currentFetchCounts = wallet.getTxFetchCounts();
|
|
125
|
+
const statusObj = {
|
|
126
|
+
status: 'syncing',
|
|
127
|
+
scanned: currentSummary.lastScannedHeight,
|
|
128
|
+
target: options.targetHeight ?? options.endHeight ?? currentSummary.lastScannedHeight,
|
|
129
|
+
notesFound: currentSummary.unspentNotesCount,
|
|
130
|
+
pendingFetches: currentFetchCounts.pendingCount,
|
|
131
|
+
failedFetches: currentFetchCounts.failedCount,
|
|
132
|
+
};
|
|
133
|
+
syncStatusMap.set(key, statusObj);
|
|
134
|
+
const syncPromise = (async () => {
|
|
135
|
+
try {
|
|
136
|
+
const result = await (0, sync_js_1.syncWallet)(client, wallet, {
|
|
137
|
+
...options,
|
|
138
|
+
onProgress: (scanned, target, notes) => {
|
|
139
|
+
statusObj.scanned = scanned;
|
|
140
|
+
statusObj.target = target;
|
|
141
|
+
statusObj.notesFound = notes;
|
|
142
|
+
try {
|
|
143
|
+
const counts = wallet.getTxFetchCounts();
|
|
144
|
+
statusObj.pendingFetches = counts.pendingCount;
|
|
145
|
+
statusObj.failedFetches = counts.failedCount;
|
|
146
|
+
}
|
|
147
|
+
catch (_) { }
|
|
148
|
+
if (options.onProgress) {
|
|
149
|
+
options.onProgress(scanned, target, notes);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
statusObj.status = 'synced';
|
|
154
|
+
statusObj.scanned = result.summary.lastScannedHeight;
|
|
155
|
+
statusObj.target = result.tipHeight;
|
|
156
|
+
statusObj.notesFound = result.summary.unspentNotesCount;
|
|
157
|
+
statusObj.pendingFetches = result.pendingFetches;
|
|
158
|
+
statusObj.failedFetches = result.failedFetches;
|
|
159
|
+
statusObj.lastSyncedAt = Date.now();
|
|
160
|
+
statusObj.error = undefined;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
statusObj.status = 'error';
|
|
164
|
+
statusObj.error = err?.message ?? String(err);
|
|
165
|
+
try {
|
|
166
|
+
const counts = wallet.getTxFetchCounts();
|
|
167
|
+
statusObj.pendingFetches = counts.pendingCount;
|
|
168
|
+
statusObj.failedFetches = counts.failedCount;
|
|
169
|
+
}
|
|
170
|
+
catch (_) { }
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
syncPromiseMap.delete(key);
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
176
|
+
syncPromiseMap.set(key, syncPromise);
|
|
177
|
+
return { ...statusObj };
|
|
178
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DecryptedTransaction, DecodedTransaction } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decrypts an Ironwood transaction wire serialization using a Unified Full Viewing Key.
|
|
4
|
+
*
|
|
5
|
+
* Recovers incoming actions (external and internal scope, marked with isChange/isInternal)
|
|
6
|
+
* and outgoing actions (via OVK recovery). Also returns txid, net fee (valueBalance),
|
|
7
|
+
* expiry height, and consensus branch ID.
|
|
8
|
+
*
|
|
9
|
+
* @param ufvk The Unified Full Viewing Key encoded string.
|
|
10
|
+
* @param network Network name ('regtest', 'testnet', 'mainnet', 'regtest-nu7').
|
|
11
|
+
* @param rawTxBytes Raw transaction serialized bytes.
|
|
12
|
+
* @param height Block height of the transaction (or target height).
|
|
13
|
+
* @returns Fully decrypted transaction details with typed values and decoded memos.
|
|
14
|
+
*/
|
|
15
|
+
export declare function decryptTransaction(ufvk: string, network: string, rawTxBytes: Uint8Array | Buffer, height: number): DecryptedTransaction;
|
|
16
|
+
/**
|
|
17
|
+
* Parses and decodes raw transaction bytes locally using core crypto parsers.
|
|
18
|
+
* Does not trust or rely on remote node's decoded JSON representation.
|
|
19
|
+
*
|
|
20
|
+
* @param rawTxBytes Serialized transaction bytes as Buffer or Uint8Array.
|
|
21
|
+
* @param network Network name (optional, defaults to 'testnet').
|
|
22
|
+
* @param height Confirmed block height if known (optional).
|
|
23
|
+
* @returns Fully decoded transaction structure with upgrade name, pool action counts,
|
|
24
|
+
* anchors, nullifiers, commitments, and ZIP 317 fee calculation.
|
|
25
|
+
*/
|
|
26
|
+
export declare function decodeTransaction(rawTxBytes: Uint8Array | Buffer, network?: string, height?: number): DecodedTransaction;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decryptTransaction = decryptTransaction;
|
|
4
|
+
exports.decodeTransaction = decodeTransaction;
|
|
5
|
+
const core_1 = require("@zcash-app/core");
|
|
6
|
+
const consensus_js_1 = require("./consensus.js");
|
|
7
|
+
/**
|
|
8
|
+
* Decrypts an Ironwood transaction wire serialization using a Unified Full Viewing Key.
|
|
9
|
+
*
|
|
10
|
+
* Recovers incoming actions (external and internal scope, marked with isChange/isInternal)
|
|
11
|
+
* and outgoing actions (via OVK recovery). Also returns txid, net fee (valueBalance),
|
|
12
|
+
* expiry height, and consensus branch ID.
|
|
13
|
+
*
|
|
14
|
+
* @param ufvk The Unified Full Viewing Key encoded string.
|
|
15
|
+
* @param network Network name ('regtest', 'testnet', 'mainnet', 'regtest-nu7').
|
|
16
|
+
* @param rawTxBytes Raw transaction serialized bytes.
|
|
17
|
+
* @param height Block height of the transaction (or target height).
|
|
18
|
+
* @returns Fully decrypted transaction details with typed values and decoded memos.
|
|
19
|
+
*/
|
|
20
|
+
function decryptTransaction(ufvk, network, rawTxBytes, height) {
|
|
21
|
+
const buf = Buffer.isBuffer(rawTxBytes) ? rawTxBytes : Buffer.from(rawTxBytes);
|
|
22
|
+
const res = (0, core_1.decryptTransaction)(ufvk, network, buf, height);
|
|
23
|
+
return {
|
|
24
|
+
txid: res.txid,
|
|
25
|
+
fee: res.fee !== null && res.fee !== undefined ? BigInt(res.fee) : null,
|
|
26
|
+
expiryHeight: res.expiryHeight,
|
|
27
|
+
consensusBranchId: res.consensusBranchId,
|
|
28
|
+
incoming: res.incoming.map((inc) => ({
|
|
29
|
+
actionIndex: inc.actionIndex,
|
|
30
|
+
value: BigInt(inc.value),
|
|
31
|
+
memo: {
|
|
32
|
+
text: inc.memo.text,
|
|
33
|
+
hex: inc.memo.hex,
|
|
34
|
+
},
|
|
35
|
+
recipient: inc.recipient,
|
|
36
|
+
isChange: inc.isChange,
|
|
37
|
+
isInternal: inc.isInternal,
|
|
38
|
+
})),
|
|
39
|
+
outgoing: res.outgoing.map((out) => ({
|
|
40
|
+
actionIndex: out.actionIndex,
|
|
41
|
+
value: BigInt(out.value),
|
|
42
|
+
memo: {
|
|
43
|
+
text: out.memo.text,
|
|
44
|
+
hex: out.memo.hex,
|
|
45
|
+
},
|
|
46
|
+
recipient: out.recipient,
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parses and decodes raw transaction bytes locally using core crypto parsers.
|
|
52
|
+
* Does not trust or rely on remote node's decoded JSON representation.
|
|
53
|
+
*
|
|
54
|
+
* @param rawTxBytes Serialized transaction bytes as Buffer or Uint8Array.
|
|
55
|
+
* @param network Network name (optional, defaults to 'testnet').
|
|
56
|
+
* @param height Confirmed block height if known (optional).
|
|
57
|
+
* @returns Fully decoded transaction structure with upgrade name, pool action counts,
|
|
58
|
+
* anchors, nullifiers, commitments, and ZIP 317 fee calculation.
|
|
59
|
+
*/
|
|
60
|
+
function decodeTransaction(rawTxBytes, network, height) {
|
|
61
|
+
const buf = Buffer.isBuffer(rawTxBytes) ? rawTxBytes : Buffer.from(rawTxBytes);
|
|
62
|
+
const res = (0, core_1.decodeTransaction)(buf, network, height);
|
|
63
|
+
const upgradeName = (0, consensus_js_1.getUpgradeName)(res.consensusBranchId);
|
|
64
|
+
return {
|
|
65
|
+
txid: res.txid,
|
|
66
|
+
version: res.version,
|
|
67
|
+
consensusBranchId: res.consensusBranchId,
|
|
68
|
+
upgradeName,
|
|
69
|
+
expiryHeight: res.expiryHeight,
|
|
70
|
+
fee: res.fee !== null && res.fee !== undefined ? BigInt(res.fee) : null,
|
|
71
|
+
feeUnknown: res.fee === null || res.fee === undefined,
|
|
72
|
+
pools: res.pools,
|
|
73
|
+
actionCounts: {
|
|
74
|
+
transparentInputs: res.actionCounts.transparentInputs,
|
|
75
|
+
transparentOutputs: res.actionCounts.transparentOutputs,
|
|
76
|
+
saplingSpends: res.actionCounts.saplingSpends,
|
|
77
|
+
saplingOutputs: res.actionCounts.saplingOutputs,
|
|
78
|
+
orchardActions: res.actionCounts.orchardActions,
|
|
79
|
+
ironwoodActions: res.actionCounts.ironwoodActions,
|
|
80
|
+
},
|
|
81
|
+
anchors: {
|
|
82
|
+
sapling: res.anchors.sapling
|
|
83
|
+
? {
|
|
84
|
+
walletOrchardOrder: res.anchors.sapling.walletOrchardOrder,
|
|
85
|
+
zebraRpcOrder: res.anchors.sapling.zebraRpcOrder,
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
orchard: res.anchors.orchard
|
|
89
|
+
? {
|
|
90
|
+
walletOrchardOrder: res.anchors.orchard.walletOrchardOrder,
|
|
91
|
+
zebraRpcOrder: res.anchors.orchard.zebraRpcOrder,
|
|
92
|
+
}
|
|
93
|
+
: undefined,
|
|
94
|
+
ironwood: res.anchors.ironwood
|
|
95
|
+
? {
|
|
96
|
+
walletOrchardOrder: res.anchors.ironwood.walletOrchardOrder,
|
|
97
|
+
zebraRpcOrder: res.anchors.ironwood.zebraRpcOrder,
|
|
98
|
+
}
|
|
99
|
+
: undefined,
|
|
100
|
+
},
|
|
101
|
+
nullifiers: res.nullifiers,
|
|
102
|
+
noteCommitments: res.noteCommitments,
|
|
103
|
+
size: res.size,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ZcashClient } from './client.js';
|
|
2
|
+
import { Account, AccountBalance, IncomingNote, SyncResult, Ufvk, WatchOnlyAccount } from './types.js';
|
|
3
|
+
export declare function detectIncoming(client: ZcashClient, key: Ufvk | WatchOnlyAccount | Account, opts?: {
|
|
4
|
+
sinceHeight?: number;
|
|
5
|
+
endHeight?: number;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
notes: IncomingNote[];
|
|
8
|
+
spentNullifiers: Set<string>;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function getBalance(client: ZcashClient, key: Ufvk | WatchOnlyAccount | Account, opts?: {
|
|
11
|
+
minConf?: number;
|
|
12
|
+
sinceHeight?: number;
|
|
13
|
+
endHeight?: number;
|
|
14
|
+
}): Promise<AccountBalance>;
|
|
15
|
+
export declare function sync(client: ZcashClient, key: Ufvk | WatchOnlyAccount | Account, opts?: {
|
|
16
|
+
sinceHeight?: number;
|
|
17
|
+
}): Promise<SyncResult>;
|
|
18
|
+
export declare function watch(client: ZcashClient, key: Ufvk | WatchOnlyAccount | Account, onNote: (note: IncomingNote) => void, opts?: {
|
|
19
|
+
pollMs?: number;
|
|
20
|
+
}): () => void;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.detectIncoming = detectIncoming;
|
|
37
|
+
exports.getBalance = getBalance;
|
|
38
|
+
exports.sync = sync;
|
|
39
|
+
exports.watch = watch;
|
|
40
|
+
const core = __importStar(require("@zcash-app/core"));
|
|
41
|
+
async function detectIncoming(client, key, opts) {
|
|
42
|
+
const ufvk = typeof key === 'string' ? key : key.getUnifiedFullViewingKey();
|
|
43
|
+
const network = client.config.network;
|
|
44
|
+
const info = await client.getInfo();
|
|
45
|
+
const currentHeight = parseInt(info.blockHeight, 10);
|
|
46
|
+
const targetEndHeight = opts?.endHeight ?? currentHeight;
|
|
47
|
+
let startHeight = opts?.sinceHeight;
|
|
48
|
+
if (startHeight === undefined) {
|
|
49
|
+
const acctBirthday = typeof key !== 'string' ? key.birthdayHeight : undefined;
|
|
50
|
+
startHeight = acctBirthday ?? 1;
|
|
51
|
+
}
|
|
52
|
+
if (startHeight > targetEndHeight) {
|
|
53
|
+
return { notes: [], spentNullifiers: new Set() };
|
|
54
|
+
}
|
|
55
|
+
const detected = [];
|
|
56
|
+
const spentNullifiers = new Set();
|
|
57
|
+
const blockStream = client.getBlockRange(startHeight, targetEndHeight);
|
|
58
|
+
let orchardTreePosition = 0;
|
|
59
|
+
let ironwoodTreePosition = 0;
|
|
60
|
+
for await (const block of blockStream) {
|
|
61
|
+
const blockHeight = typeof block.height === 'string' ? parseInt(block.height, 10) : Number(block.height);
|
|
62
|
+
if (!block.vtx)
|
|
63
|
+
continue;
|
|
64
|
+
for (const tx of block.vtx) {
|
|
65
|
+
const rawTxid = tx.txid ?? tx.hash;
|
|
66
|
+
const txid = Buffer.isBuffer(rawTxid)
|
|
67
|
+
? Buffer.from(rawTxid).reverse().toString('hex')
|
|
68
|
+
: String(rawTxid ?? '');
|
|
69
|
+
// Record any spent nullifiers in actions
|
|
70
|
+
if (tx.actions && tx.actions.length > 0) {
|
|
71
|
+
for (let i = 0; i < tx.actions.length; i++) {
|
|
72
|
+
const action = tx.actions[i];
|
|
73
|
+
const hasCmx = action.cmx && action.cmx.length === 32;
|
|
74
|
+
const currentPos = hasCmx ? orchardTreePosition++ : undefined;
|
|
75
|
+
if (action.nullifier && action.nullifier.length === 32) {
|
|
76
|
+
const nfHex = Buffer.from(action.nullifier).toString('hex');
|
|
77
|
+
spentNullifiers.add(nfHex);
|
|
78
|
+
}
|
|
79
|
+
// In protobuf keepCase: true, field is ciphertext
|
|
80
|
+
const encCiphertext = action.ciphertext || action.cipherText;
|
|
81
|
+
if (!encCiphertext)
|
|
82
|
+
continue;
|
|
83
|
+
const result = core.tryDecryptAction(ufvk, network, action.ephemeralKey, action.cmx, encCiphertext, false, i, action.nullifier);
|
|
84
|
+
if (result) {
|
|
85
|
+
detected.push({
|
|
86
|
+
pool: 'orchard',
|
|
87
|
+
value: BigInt(result.valueZatoshis),
|
|
88
|
+
txid,
|
|
89
|
+
height: blockHeight,
|
|
90
|
+
recipient: result.recipient,
|
|
91
|
+
nullifier: result.nullifier,
|
|
92
|
+
rho: result.rho,
|
|
93
|
+
rseed: result.rseed,
|
|
94
|
+
cmx: hasCmx ? Buffer.from(action.cmx).toString('hex') : undefined,
|
|
95
|
+
treePosition: currentPos,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Record any spent nullifiers in ironwoodActions
|
|
101
|
+
if (tx.ironwoodActions && tx.ironwoodActions.length > 0) {
|
|
102
|
+
for (let i = 0; i < tx.ironwoodActions.length; i++) {
|
|
103
|
+
const action = tx.ironwoodActions[i];
|
|
104
|
+
const hasCmx = action.cmx && action.cmx.length === 32;
|
|
105
|
+
const currentPos = hasCmx ? ironwoodTreePosition++ : undefined;
|
|
106
|
+
if (action.nullifier && action.nullifier.length === 32) {
|
|
107
|
+
const nfHex = Buffer.from(action.nullifier).toString('hex');
|
|
108
|
+
spentNullifiers.add(nfHex);
|
|
109
|
+
}
|
|
110
|
+
const encCiphertext = action.ciphertext || action.cipherText;
|
|
111
|
+
if (!encCiphertext)
|
|
112
|
+
continue;
|
|
113
|
+
const result = core.tryDecryptAction(ufvk, network, action.ephemeralKey, action.cmx, encCiphertext, true, i, action.nullifier);
|
|
114
|
+
if (result) {
|
|
115
|
+
detected.push({
|
|
116
|
+
pool: 'ironwood',
|
|
117
|
+
value: BigInt(result.valueZatoshis),
|
|
118
|
+
txid,
|
|
119
|
+
height: blockHeight,
|
|
120
|
+
recipient: result.recipient,
|
|
121
|
+
nullifier: result.nullifier,
|
|
122
|
+
rho: result.rho,
|
|
123
|
+
rseed: result.rseed,
|
|
124
|
+
cmx: hasCmx ? Buffer.from(action.cmx).toString('hex') : undefined,
|
|
125
|
+
treePosition: currentPos,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { notes: detected, spentNullifiers };
|
|
133
|
+
}
|
|
134
|
+
async function getBalance(client, key, opts) {
|
|
135
|
+
const minConf = opts?.minConf ?? 1;
|
|
136
|
+
const info = await client.getInfo();
|
|
137
|
+
const currentHeight = parseInt(info.blockHeight, 10);
|
|
138
|
+
const { notes, spentNullifiers } = await detectIncoming(client, key, opts);
|
|
139
|
+
const confirmed = {
|
|
140
|
+
orchard: 0n,
|
|
141
|
+
ironwood: 0n,
|
|
142
|
+
sapling: 0n,
|
|
143
|
+
transparent: 0n,
|
|
144
|
+
total: 0n,
|
|
145
|
+
};
|
|
146
|
+
const pending = {
|
|
147
|
+
orchard: 0n,
|
|
148
|
+
ironwood: 0n,
|
|
149
|
+
sapling: 0n,
|
|
150
|
+
transparent: 0n,
|
|
151
|
+
total: 0n,
|
|
152
|
+
};
|
|
153
|
+
for (const n of notes) {
|
|
154
|
+
if (n.nullifier && spentNullifiers.has(n.nullifier)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const confs = currentHeight >= n.height ? currentHeight - n.height + 1 : 0;
|
|
158
|
+
const target = confs >= minConf ? confirmed : pending;
|
|
159
|
+
if (n.pool === 'orchard') {
|
|
160
|
+
target.orchard += n.value;
|
|
161
|
+
}
|
|
162
|
+
else if (n.pool === 'ironwood') {
|
|
163
|
+
target.ironwood += n.value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let transparentConfirmed = 0n;
|
|
167
|
+
let transparentPending = 0n;
|
|
168
|
+
const tAddr = typeof key !== 'string' && key.getTransparentAddress ? key.getTransparentAddress() : undefined;
|
|
169
|
+
if (tAddr) {
|
|
170
|
+
try {
|
|
171
|
+
const utxos = await client.getAddressUtxos([tAddr]);
|
|
172
|
+
for (const u of utxos) {
|
|
173
|
+
if (u.valueZat) {
|
|
174
|
+
const uHeight = u.height ? Number(u.height) : 0;
|
|
175
|
+
const confs = uHeight > 0 && currentHeight >= uHeight ? currentHeight - uHeight + 1 : 0;
|
|
176
|
+
if (confs >= minConf) {
|
|
177
|
+
transparentConfirmed += BigInt(u.valueZat);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
transparentPending += BigInt(u.valueZat);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
// transparent index not available or empty
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
confirmed.transparent = transparentConfirmed;
|
|
190
|
+
pending.transparent = transparentPending;
|
|
191
|
+
confirmed.total = confirmed.orchard + confirmed.ironwood + confirmed.sapling + confirmed.transparent;
|
|
192
|
+
pending.total = pending.orchard + pending.ironwood + pending.sapling + pending.transparent;
|
|
193
|
+
return {
|
|
194
|
+
orchard: confirmed.orchard,
|
|
195
|
+
ironwood: confirmed.ironwood,
|
|
196
|
+
sapling: confirmed.sapling,
|
|
197
|
+
transparent: confirmed.transparent,
|
|
198
|
+
total: confirmed.total,
|
|
199
|
+
confirmed,
|
|
200
|
+
pending,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function sync(client, key, opts) {
|
|
204
|
+
const info = await client.getInfo();
|
|
205
|
+
const currentHeight = parseInt(info.blockHeight, 10);
|
|
206
|
+
const { notes } = await detectIncoming(client, key, opts);
|
|
207
|
+
const balances = await getBalance(client, key);
|
|
208
|
+
return {
|
|
209
|
+
syncedHeight: currentHeight,
|
|
210
|
+
newNotes: notes,
|
|
211
|
+
balances,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function watch(client, key, onNote, opts) {
|
|
215
|
+
const pollInterval = opts?.pollMs ?? 15000;
|
|
216
|
+
let active = true;
|
|
217
|
+
let lastScannedHeight = 0;
|
|
218
|
+
const poll = async () => {
|
|
219
|
+
if (!active)
|
|
220
|
+
return;
|
|
221
|
+
try {
|
|
222
|
+
const info = await client.getInfo();
|
|
223
|
+
const currentHeight = parseInt(info.blockHeight, 10);
|
|
224
|
+
if (lastScannedHeight === 0) {
|
|
225
|
+
// First run: establish tip without re-emitting history unless birthdayHeight is set
|
|
226
|
+
const birthday = typeof key !== 'string' ? key.birthdayHeight : undefined;
|
|
227
|
+
lastScannedHeight = birthday ? birthday - 1 : currentHeight;
|
|
228
|
+
}
|
|
229
|
+
if (currentHeight > lastScannedHeight) {
|
|
230
|
+
const nextStart = lastScannedHeight + 1;
|
|
231
|
+
const { notes } = await detectIncoming(client, key, {
|
|
232
|
+
sinceHeight: nextStart,
|
|
233
|
+
endHeight: currentHeight,
|
|
234
|
+
});
|
|
235
|
+
for (const note of notes) {
|
|
236
|
+
onNote(note);
|
|
237
|
+
}
|
|
238
|
+
lastScannedHeight = currentHeight;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
// transient network poll error
|
|
243
|
+
}
|
|
244
|
+
if (active) {
|
|
245
|
+
setTimeout(poll, pollInterval);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
poll();
|
|
249
|
+
return () => {
|
|
250
|
+
active = false;
|
|
251
|
+
};
|
|
252
|
+
}
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Account, TxResult } from './types.js';
|
|
2
|
+
import { ZcashClient } from './client.js';
|
|
3
|
+
export declare namespace dev {
|
|
4
|
+
const fixtures: {
|
|
5
|
+
mnemonics: readonly string[];
|
|
6
|
+
account(index: number, network?: "regtest" | "testnet" | "mainnet"): Account;
|
|
7
|
+
};
|
|
8
|
+
function mine(client: ZcashClient, blocks: number): Promise<void>;
|
|
9
|
+
function fund(client: ZcashClient, to: string, value: bigint): Promise<TxResult>;
|
|
10
|
+
function reset(client: ZcashClient): Promise<void>;
|
|
11
|
+
function faucet(address: string, opts?: {
|
|
12
|
+
ztapUrl?: string;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
status: 'sent' | 'open-url';
|
|
15
|
+
txid?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export declare const testkit: {
|
|
20
|
+
fixtures: {
|
|
21
|
+
mnemonics: readonly string[];
|
|
22
|
+
account(index: number, network?: "regtest" | "testnet" | "mainnet"): Account;
|
|
23
|
+
};
|
|
24
|
+
withRegtest(fn: (ctx: {
|
|
25
|
+
client: ZcashClient;
|
|
26
|
+
fund: typeof dev.fund;
|
|
27
|
+
}) => Promise<void>): Promise<void>;
|
|
28
|
+
};
|