Skip to content

Commit 647f6b9

Browse files
added RecordScanner class as an implementation of the RecordProvider interface
1 parent 627631e commit 647f6b9

File tree

2 files changed

+339
-0
lines changed

2 files changed

+339
-0
lines changed

sdk/src/browser.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
NetworkRecordProvider,
5353
RecordProvider,
5454
} from "./record-provider.js";
55+
import { RecordScanner } from "./record-scanner.js";
5556

5657
// @TODO: This function is no longer needed, remove it.
5758
async function initializeWasm() {
@@ -177,6 +178,7 @@ export {
177178
RecordsFilter,
178179
RecordsResponseFilter,
179180
RecordProvider,
181+
RecordScanner,
180182
RecordSearchParams,
181183
SolutionJSON,
182184
SolutionsJSON,

sdk/src/record-scanner.ts

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import { Account } from "./account";
2+
import { EncryptedRecord } from "./models/record-provider/encryptedRecord";
3+
import { OwnedFilter } from "./models/record-scanner/ownedFilter";
4+
import { OwnedRecord } from "./models/record-provider/ownedRecord";
5+
import { RecordProvider } from "./record-provider";
6+
import { RecordPlaintext } from "./wasm";
7+
import { RecordSearchParams } from "./models/record-provider/recordSearchParams";
8+
import { RecordsFilter } from "./models/record-scanner/recordsFilter";
9+
import { RecordsResponseFilter } from "./models/record-scanner/recordsResponseFilter";
10+
import { RegistrationRequest } from "./models/record-scanner/registrationRequest";
11+
12+
type RecordScannerOptions = {
13+
url: string;
14+
account?: Account;
15+
apiKey?: string | { header: string, value: string };
16+
}
17+
18+
/**
19+
* RecordScanner is a RecordProvider implementation that uses the record scanner service to find records.
20+
*
21+
* @example
22+
* const account = new Account({ privateKey: 'APrivateKey1...' });
23+
*
24+
* const recordScanner = new RecordScanner({ url: "https://record-scanner.aleo.org" });
25+
* recordScanner.setAccount(account);
26+
* recordScanner.setApiKey("your-api-key");
27+
* const uuid = await recordScanner.register(0);
28+
*
29+
* const filter = {
30+
* uuid,
31+
* filter: {
32+
* program: "credits.aleo",
33+
* records: ["credits"],
34+
* },
35+
* responseFilter: {
36+
* commitment: true,
37+
* owner: true,
38+
* tag: true,
39+
* tag?: boolean;
40+
* sender: true,
41+
* spent: true,
42+
* record_ciphertext: true,
43+
* block_height: true;
44+
* block_timestamp: true;
45+
* output_index: true;
46+
* record_name: true;
47+
* function_name: true;
48+
* program_name: true;
49+
* transition_id: true;
50+
* transaction_id: true;
51+
* transaction_index: true;
52+
* transition_index: true;
53+
* },
54+
* unspent: true,
55+
* };
56+
*
57+
* const records = await recordScanner.findRecords(filter);
58+
*/
59+
class RecordScanner implements RecordProvider {
60+
account?: Account;
61+
readonly url: string;
62+
private apiKey?: { header: string, value: string };
63+
private uuid?: string;
64+
65+
constructor(options: RecordScannerOptions) {
66+
this.url = options.url;
67+
this.account = options.account;
68+
this.apiKey = typeof options.apiKey === "string" ? { header: "X-Provable-API-Key", value: options.apiKey } : options.apiKey;
69+
}
70+
71+
/**
72+
* Set the account to use for the record scanner.
73+
*
74+
* @param {Account} account The account to use for the record scanner.
75+
*/
76+
async setAccount(account: Account): Promise<void> {
77+
this.uuid = undefined;
78+
this.account = account;
79+
}
80+
81+
/**
82+
* Set the API key to use for the record scanner.
83+
*
84+
* @param {string} apiKey The API key to use for the record scanner.
85+
*/
86+
async setApiKey(apiKey: string | { header: string, value: string }): Promise<void> {
87+
this.apiKey = typeof apiKey === "string" ? { header: "X-Provable-API-Key", value: apiKey } : apiKey;
88+
}
89+
90+
/**
91+
* Register the account with the record scanner service.
92+
*
93+
* @param {number} startBlock The block height to start scanning from.
94+
* @returns {Promise<RegistrationResponse>} The response from the record scanner service.
95+
*/
96+
async register(startBlock: number): Promise<RegistrationResponse> {
97+
let request: RegistrationRequest;
98+
if (!this.account) {
99+
throw new Error("Account not set");
100+
} else {
101+
request = {
102+
view_key: this.account.viewKey().to_string(),
103+
start: startBlock,
104+
};
105+
}
106+
107+
try {
108+
const response = await this.request(
109+
new Request(`${this.url}/register`, {
110+
method: "POST",
111+
headers: { "Content-Type": "application/json" },
112+
body: JSON.stringify(request),
113+
})
114+
);
115+
116+
const data = await response.json();
117+
this.uuid = data.uuid;
118+
return data;
119+
} catch (error) {
120+
console.error(`Failed to register view key: ${error}`);
121+
throw error;
122+
}
123+
}
124+
125+
/**
126+
* Get encrypted records from the record scanner service.
127+
*
128+
* @param {RecordsFilter} recordsFilter The filter to use to find the records and filter the response.
129+
* @returns {Promise<EncryptedRecord[]>} The encrypted records.
130+
*/
131+
async encryptedRecords(recordsFilter: RecordsFilter): Promise<EncryptedRecord[]> {
132+
try {
133+
const response = await this.request(
134+
new Request(`${this.url}/records/encrypted`, {
135+
method: "POST",
136+
headers: { "Content-Type": "application/json" },
137+
body: JSON.stringify(recordsFilter),
138+
}),
139+
);
140+
141+
return await response.json();
142+
} catch (error) {
143+
console.error(`Failed to get encrypted records: ${error}`);
144+
throw error;
145+
}
146+
}
147+
148+
/**
149+
* Check if a list of serial numbers exist in the record scanner service.
150+
*
151+
* @param {string[]} serialNumbers The serial numbers to check.
152+
* @returns {Promise<Record<string, boolean>>} Map of Aleo Record serial numbers and whether they appeared in any inputs on chain. If boolean corresponding to the Serial Number has a true value, that Record is considered spent by the Aleo Network.
153+
*/
154+
async checkSerialNumbers(serialNumbers: string[]): Promise<Record<string, boolean>> {
155+
try {
156+
const response = await this.request(
157+
new Request(`${this.url}/records/sns`, {
158+
method: "POST",
159+
headers: { "Content-Type": "application/json" },
160+
body: JSON.stringify(serialNumbers),
161+
}),
162+
);
163+
164+
return await response.json();
165+
} catch (error) {
166+
console.error(`Failed to check if serial numbers exist: ${error}`);
167+
throw error;
168+
}
169+
}
170+
171+
/**
172+
* Check if a list of tags exist in the record scanner service.
173+
*
174+
* @param {string[]} tags The tags to check.
175+
* @returns {Promise<Record<string, boolean>>} Map of Aleo Record tags and whether they appeared in any inputs on chain. If boolean corresponding to the tag has a true value, that Record is considered spent by the Aleo Network.
176+
*/
177+
async checkTags(tags: string[]): Promise<Record<string, boolean>> {
178+
try {
179+
const response = await this.request(
180+
new Request(`${this.url}/records/tags`, {
181+
method: "POST",
182+
headers: { "Content-Type": "application/json" },
183+
body: JSON.stringify(tags),
184+
}),
185+
);
186+
187+
return await response.json();
188+
} catch (error) {
189+
console.error(`Failed to check if tags exist: ${error}`);
190+
throw error;
191+
}
192+
}
193+
194+
/**
195+
* Find a record in the record scanner service.
196+
*
197+
* @param {OwnedFilter} searchParameters The filter to use to find the record.
198+
* @returns {Promise<OwnedRecord>} The record.
199+
*/
200+
async findRecord(searchParameters: OwnedFilter): Promise<OwnedRecord> {
201+
try {
202+
const records = await this.findRecords(searchParameters);
203+
204+
if (records.length > 0) {
205+
return records[0];
206+
}
207+
208+
throw new Error("Record not found");
209+
} catch (error) {
210+
console.error(`Failed to find record: ${error}`);
211+
throw error;
212+
}
213+
}
214+
215+
/**
216+
* Find records in the record scanner service.
217+
*
218+
* @param {OwnedFilter} filter The filter to use to find the records.
219+
* @returns {Promise<OwnedRecord[]>} The records.
220+
*/
221+
async findRecords(filter: OwnedFilter): Promise<OwnedRecord[]> {
222+
if (!this.uuid) {
223+
throw new Error("Not registered");
224+
}
225+
226+
filter.uuid = this.uuid;
227+
228+
try {
229+
const response = await this.request(
230+
new Request(`${this.url}/records/owned`, {
231+
method: "POST",
232+
headers: { "Content-Type": "application/json" },
233+
body: JSON.stringify(filter),
234+
}),
235+
);
236+
237+
return await response.json();
238+
} catch (error) {
239+
console.error(`Failed to get owned records: ${error}`);
240+
throw error;
241+
}
242+
}
243+
244+
/**
245+
* Find a credits record in the record scanner service.
246+
*
247+
* @param {number} microcredits The amount of microcredits to find.
248+
* @param {OwnedFilter} searchParameters The filter to use to find the record.
249+
* @returns {Promise<OwnedRecord>} The record.
250+
*/
251+
async findCreditsRecord(microcredits: number, searchParameters: OwnedFilter): Promise<OwnedRecord> {
252+
try {
253+
const records = await this.findRecords({
254+
decrypt: true,
255+
unspent: searchParameters.unspent ?? false,
256+
filter: {
257+
start: searchParameters.filter?.start ?? 0,
258+
program: "credits.aleo",
259+
record: "credits",
260+
},
261+
uuid: this.uuid,
262+
});
263+
264+
const record = records.find(record => {
265+
const plaintext = RecordPlaintext.fromString(record.record_plaintext ?? '');
266+
const amountStr = plaintext.getMember("microcredits").toString();
267+
const amount = parseInt(amountStr.replace("u64", ""));
268+
return amount >= microcredits;
269+
});
270+
271+
if (!record) {
272+
throw new Error("Record not found");
273+
}
274+
275+
return record;
276+
} catch (error) {
277+
console.error(`Failed to find credits record: ${error}`);
278+
throw error;
279+
}
280+
}
281+
282+
/**
283+
* Find credits records using a record scanning service.
284+
*
285+
* @param {number[]} microcreditAmounts The amounts of microcredits to find.
286+
* @param {OwnedFilter} searchParameters The filter to use to find the records.
287+
* @returns {Promise<OwnedRecord[]>} The records
288+
*/
289+
async findCreditsRecords(microcreditAmounts: number[], searchParameters: OwnedFilter): Promise<OwnedRecord[]> {
290+
try {
291+
const records = await this.findRecords({
292+
decrypt: true,
293+
unspent: searchParameters.unspent ?? false,
294+
filter: {
295+
start: searchParameters.filter?.start ?? 0,
296+
program: "credits.aleo",
297+
record: "credits",
298+
},
299+
uuid: this.uuid,
300+
});
301+
return records.filter(record => {
302+
const plaintext = RecordPlaintext.fromString(record.record_plaintext ?? '');
303+
const amount = plaintext.getMember("microcredits").toString();
304+
return microcreditAmounts.includes(parseInt(amount.replace("u64", "")));
305+
});
306+
} catch (error) {
307+
console.error(`Failed to find credits records: ${error}`);
308+
throw error;
309+
}
310+
}
311+
312+
/**
313+
* Wrapper function to make a request to the record scanner service and handle any errors.
314+
*
315+
* @param {Request} req The request to make.
316+
* @returns {Promise<Response>} The response.
317+
*/
318+
private async request(req: Request): Promise<Response> {
319+
try {
320+
if (this.apiKey) {
321+
req.headers.set(this.apiKey.header, this.apiKey.value);
322+
}
323+
const response = await fetch(req);
324+
325+
if (!response.ok) {
326+
throw new Error(await response.text() ?? `Request to ${req.url} failed with status ${response.status}`);
327+
}
328+
329+
return response;
330+
} catch (error) {
331+
console.error(`Failed to make request to ${req.url}: ${error}`);
332+
throw error;
333+
}
334+
}
335+
}
336+
337+
export { RecordScanner };

0 commit comments

Comments
 (0)