@fastnear/api 0.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/near.js ADDED
@@ -0,0 +1,567 @@
1
+ import Big from "big.js";
2
+ import { WalletAdapter } from "@fastnear/wallet-adapter";
3
+ import * as crypto from "./crypto";
4
+ import {
5
+ deepCopy,
6
+ lsGet,
7
+ lsSet,
8
+ toBase58,
9
+ toBase64,
10
+ tryParseJson,
11
+ } from "./utils";
12
+ import { sha256, signBytes } from "./crypto";
13
+ import {
14
+ serializeSignedTransaction,
15
+ serializeTransaction,
16
+ } from "./transaction";
17
+
18
+ Big.DP = 27;
19
+
20
+ // Constants
21
+ const MaxBlockDelayMs = 1000 * 60 * 60 * 6; // 6 hours
22
+
23
+ const DEFAULT_NETWORK_ID = "mainnet";
24
+ const NETWORKS = {
25
+ testnet: {
26
+ networkId: "testnet",
27
+ nodeUrl: "https://rpc.testnet.fastnear.com/",
28
+ },
29
+ mainnet: {
30
+ networkId: "mainnet",
31
+ nodeUrl: "https://rpc.mainnet.fastnear.com/",
32
+ },
33
+ };
34
+
35
+ // State
36
+ let _config = { ...NETWORKS[DEFAULT_NETWORK_ID] };
37
+
38
+ let _state;
39
+ {
40
+ const privateKey = lsGet("privateKey");
41
+ _state = {
42
+ accountId: lsGet("accountId"),
43
+ accessKeyContractId: lsGet("accessKeyContractId"),
44
+ lastWalletId: lsGet("lastWalletId"),
45
+ privateKey,
46
+ publicKey: privateKey ? crypto.publicKeyFromPrivate(privateKey) : null,
47
+ };
48
+ console.log("Initial state:", _state);
49
+ }
50
+
51
+ const _txHistory = {};
52
+ const _eventListeners = {
53
+ account: new Set(),
54
+ tx: new Set(),
55
+ };
56
+
57
+ function updateState(newState) {
58
+ const oldState = _state;
59
+ _state = { ..._state, ...newState };
60
+ lsSet("accountId", _state.accountId);
61
+ lsSet("privateKey", _state.privateKey);
62
+ lsSet("lastWalletId", _state.lastWalletId);
63
+ lsSet("accessKeyContractId", _state.accessKeyContractId);
64
+ if (
65
+ newState.hasOwnProperty("privateKey") &&
66
+ newState.privateKey !== oldState.privateKey
67
+ ) {
68
+ _state.publicKey = newState.privateKey
69
+ ? crypto.publicKeyFromPrivate(newState.privateKey)
70
+ : null;
71
+ lsSet("nonce", null);
72
+ }
73
+ if (newState.accountId !== oldState.accountId) {
74
+ notifyAccountListeners(newState.accountId);
75
+ }
76
+ }
77
+
78
+ function updateTxHistory(txStatus) {
79
+ const txId = txStatus.txId;
80
+ _txHistory[txId] = { ...(_txHistory[txId] ?? {}), ...txStatus };
81
+ notifyTxListeners(_txHistory[txId]);
82
+ }
83
+
84
+ function onAdapterStateUpdate(state) {
85
+ console.log("Adapter state update:", state);
86
+ updateState({
87
+ privateKey: state.privateKey,
88
+ accountId: state.accountId,
89
+ lastWalletId: state.lastWalletId,
90
+ });
91
+ }
92
+
93
+ // Create adapter instance
94
+ const _adapter = new WalletAdapter({
95
+ onStateUpdate: onAdapterStateUpdate,
96
+ });
97
+
98
+ // Utils
99
+ function parseJsonFromBytes(bytes) {
100
+ try {
101
+ return JSON.parse(Buffer.from(bytes).toString());
102
+ } catch (e) {
103
+ try {
104
+ return Buffer.from(bytes);
105
+ } catch (e) {
106
+ return bytes;
107
+ }
108
+ }
109
+ }
110
+
111
+ function withBlockId(params, blockId) {
112
+ return blockId === "final" || blockId === "optimistic"
113
+ ? { ...params, finality: blockId }
114
+ : !!blockId
115
+ ? { ...params, block_id: blockId }
116
+ : { ...params, finality: "optimistic" };
117
+ }
118
+
119
+ async function queryRpc(method, params) {
120
+ const response = await fetch(_config.nodeUrl, {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({
124
+ jsonrpc: "2.0",
125
+ id: `fastnear-${Date.now()}`,
126
+ method,
127
+ params,
128
+ }),
129
+ });
130
+
131
+ const result = await response.json();
132
+ if (result.error) {
133
+ throw new Error(JSON.stringify(result.error));
134
+ }
135
+ return result.result;
136
+ }
137
+
138
+ function sendTxToRpc(signedTxBase64, waitUntil, txId) {
139
+ queryRpc("send_tx", {
140
+ signed_tx_base64: signedTxBase64,
141
+ wait_until: waitUntil ?? "INCLUDED",
142
+ })
143
+ .then((result) => {
144
+ updateTxHistory({
145
+ txId,
146
+ status: "Included",
147
+ });
148
+ queryRpc("tx", {
149
+ tx_hash: _txHistory[txId].txHash,
150
+ sender_account_id: _txHistory[txId].tx.signerId,
151
+ wait_until: "EXECUTED_OPTIMISTIC",
152
+ })
153
+ .then((result) => {
154
+ updateTxHistory({
155
+ txId,
156
+ status: "Executed",
157
+ result,
158
+ });
159
+ })
160
+ .catch((error) => {
161
+ updateTxHistory({
162
+ txId,
163
+ status: "ErrorAfterIncluded",
164
+ error: tryParseJson(error.message),
165
+ });
166
+ });
167
+ })
168
+ .catch((error) => {
169
+ updateTxHistory({
170
+ txId,
171
+ status: "Error",
172
+ error: tryParseJson(error.message),
173
+ });
174
+ });
175
+ }
176
+
177
+ // Event Notifiers
178
+ function notifyAccountListeners(accountId) {
179
+ _eventListeners.account.forEach((callback) => {
180
+ try {
181
+ callback(accountId);
182
+ } catch (e) {
183
+ console.error(e);
184
+ }
185
+ });
186
+ }
187
+
188
+ function notifyTxListeners(tx) {
189
+ _eventListeners.tx.forEach((callback) => {
190
+ try {
191
+ callback(deepCopy(tx));
192
+ } catch (e) {
193
+ console.error(e);
194
+ }
195
+ });
196
+ }
197
+
198
+ function convertUnit(s, ...args) {
199
+ // Reconstruct raw string from template literal
200
+ if (Array.isArray(s)) {
201
+ s = s.reduce((acc, part, i) => {
202
+ return acc + (args[i - 1] || "") + part;
203
+ });
204
+ }
205
+ // Convert from `100 NEAR` into yoctoNear
206
+ if (typeof s == "string") {
207
+ let match = s.match(/([0-9.,_]+)\s*([a-zA-Z]+)?/);
208
+ if (match) {
209
+ let amount = match[1].replace(/[_,]/g, "");
210
+ let unitPart = match[2];
211
+ if (unitPart) {
212
+ switch (unitPart.toLowerCase()) {
213
+ case "near":
214
+ return Big(amount).mul(Big(10).pow(24)).toFixed(0);
215
+ case "tgas":
216
+ return Big(amount).mul(Big(10).pow(12)).toFixed(0);
217
+ case "ggas":
218
+ return Big(amount).mul(Big(10).pow(9)).toFixed(0);
219
+ case "gas" || "yoctonear":
220
+ return Big(amount).toFixed(0);
221
+ default:
222
+ throw new Error(`Unknown unit: ${unit}`);
223
+ }
224
+ } else {
225
+ return Big(amount).toFixed(0);
226
+ }
227
+ }
228
+ }
229
+ return Big(s).toFixed(0);
230
+ }
231
+
232
+ // Core API Implementation
233
+ const api = {
234
+ // Context
235
+ get accountId() {
236
+ return _state.accountId;
237
+ },
238
+
239
+ get publicKey() {
240
+ return _state.publicKey;
241
+ },
242
+
243
+ config(newConfig) {
244
+ if (newConfig) {
245
+ if (newConfig.networkId && _config.networkId !== newConfig.networkId) {
246
+ throw new Error("TODO: Network ID change should handle scope");
247
+ }
248
+ _config = { ..._config, ...newConfig };
249
+ }
250
+ return _config;
251
+ },
252
+
253
+ get authStatus() {
254
+ if (!_state.accountId) return "SignedOut";
255
+
256
+ // Check for limited access key
257
+ const accessKey = _state.publicKey;
258
+ const contractId = _state.accessKeyContractId;
259
+ if (accessKey) {
260
+ return {
261
+ type: "SignedInWithLimitedAccessKey",
262
+ accessKey,
263
+ contractId,
264
+ };
265
+ }
266
+ return "SignedIn";
267
+ },
268
+
269
+ // Query Methods
270
+ async view({ contractId, methodName, args, argsBase64, blockId }) {
271
+ const encodedArgs =
272
+ argsBase64 || (args ? toBase64(JSON.stringify(args)) : "");
273
+
274
+ const result = await queryRpc(
275
+ "query",
276
+ withBlockId(
277
+ {
278
+ request_type: "call_function",
279
+ account_id: contractId,
280
+ method_name: methodName,
281
+ args_base64: encodedArgs,
282
+ },
283
+ blockId,
284
+ ),
285
+ );
286
+
287
+ return parseJsonFromBytes(result.result);
288
+ },
289
+
290
+ async account({ accountId, blockId }) {
291
+ return queryRpc(
292
+ "query",
293
+ withBlockId(
294
+ {
295
+ request_type: "view_account",
296
+ account_id: accountId,
297
+ },
298
+ blockId,
299
+ ),
300
+ );
301
+ },
302
+
303
+ async block({ blockId }) {
304
+ return queryRpc("block", withBlockId({}, blockId));
305
+ },
306
+
307
+ async accessKey({ accountId, publicKey, blockId }) {
308
+ return queryRpc(
309
+ "query",
310
+ withBlockId(
311
+ {
312
+ request_type: "view_access_key",
313
+ account_id: accountId,
314
+ public_key: publicKey,
315
+ },
316
+ blockId,
317
+ ),
318
+ );
319
+ },
320
+
321
+ async tx({ txHash, accountId }) {
322
+ return queryRpc("tx", [txHash, accountId]);
323
+ },
324
+
325
+ localTxHistory() {
326
+ return [..._txHistory];
327
+ },
328
+
329
+ // Transaction Methods
330
+ async sendTx({ receiverId, actions, waitUntil }) {
331
+ if (!_state.accountId) {
332
+ throw new Error("Not signed in");
333
+ }
334
+
335
+ if (receiverId !== _state.accessKeyContractId) {
336
+ // _adapter.sendTransaction();
337
+ throw new Error("Need to use walletAdapter. Not implemented yet");
338
+ }
339
+
340
+ const signerId = _state.accountId;
341
+ const publicKey = _state.publicKey;
342
+ const privateKey = _state.privateKey;
343
+
344
+ const toDoPromises = {};
345
+ let nonce = lsGet("nonce");
346
+ if (!nonce) {
347
+ toDoPromises.nonce = this.accessKey({
348
+ accountId: signerId,
349
+ publicKey,
350
+ }).then((accessKey) => {
351
+ lsSet("nonce", accessKey.nonce);
352
+ return accessKey.nonce;
353
+ });
354
+ }
355
+ let block = lsGet("block");
356
+ if (
357
+ !block ||
358
+ parseFloat(block.header.timestamp_nanosec) / 1e6 + MaxBlockDelayMs <
359
+ Date.now()
360
+ ) {
361
+ toDoPromises.block = this.block({ blockId: "final" }).then((block) => {
362
+ block = {
363
+ header: {
364
+ prev_hash: block.header.prev_hash,
365
+ timestamp_nanosec: block.header.timestamp_nanosec,
366
+ },
367
+ };
368
+ lsSet("block", block);
369
+ return block;
370
+ });
371
+ }
372
+
373
+ if (Object.keys(toDoPromises).length > 0) {
374
+ let results = await Promise.all(Object.values(toDoPromises));
375
+ for (let i = 0; i < results.length; i++) {
376
+ if (Object.keys(toDoPromises)[i] === "nonce") {
377
+ nonce = results[i];
378
+ } else if (Object.keys(toDoPromises)[i] === "block") {
379
+ block = results[i];
380
+ }
381
+ }
382
+ }
383
+
384
+ const newNonce = nonce + 1;
385
+ lsSet("nonce", newNonce);
386
+ const blockHash = block.header.prev_hash;
387
+
388
+ const txId = `tx-${Date.now()}-${Math.random()}`;
389
+
390
+ const jsonTransaction = {
391
+ signerId,
392
+ publicKey,
393
+ nonce: newNonce,
394
+ receiverId,
395
+ blockHash,
396
+ actions,
397
+ };
398
+
399
+ console.log("Transaction:", jsonTransaction);
400
+ const transaction = serializeTransaction(jsonTransaction);
401
+ const txHash = toBase58(sha256(transaction));
402
+ const signature = crypto.signHash(txHash, privateKey);
403
+ const singedTransaction = serializeSignedTransaction(
404
+ jsonTransaction,
405
+ signature,
406
+ );
407
+ const signedTxBase64 = toBase64(singedTransaction);
408
+
409
+ updateTxHistory({
410
+ status: "Pending",
411
+ txId,
412
+ tx: deepCopy(jsonTransaction),
413
+ signature,
414
+ signedTxBase64,
415
+ txHash,
416
+ });
417
+
418
+ sendTxToRpc(signedTxBase64, waitUntil, txId);
419
+
420
+ return txId;
421
+ },
422
+
423
+ // Authentication Methods
424
+ async requestSignIn({ contractId }) {
425
+ updateState({
426
+ accessKeyContractId: contractId,
427
+ accountId: null,
428
+ privateKey: null,
429
+ });
430
+ const result = await _adapter.signIn({
431
+ networkId: _config.networkId,
432
+ contractId,
433
+ });
434
+ console.log("Sign in result:", result);
435
+ if (result.error) {
436
+ throw new Error(`Wallet error: ${result.error}`);
437
+ }
438
+ if (result.url) {
439
+ console.log("Redirecting to wallet:", result.url);
440
+ window.location.href = result.url;
441
+ } else if (result.accountId) {
442
+ updateState({
443
+ accountId: result.accountId,
444
+ });
445
+ }
446
+ },
447
+
448
+ signOut() {
449
+ updateState({
450
+ accountId: null,
451
+ privateKey: null,
452
+ contractId: null,
453
+ });
454
+
455
+ // TODO: Implement actual wallet integration
456
+ },
457
+
458
+ // Event Handlers
459
+ onAccount(callback) {
460
+ _eventListeners.account.add(callback);
461
+ },
462
+
463
+ onTx(callback) {
464
+ _eventListeners.tx.add(callback);
465
+ },
466
+
467
+ // Action Helpers
468
+ actions: {
469
+ functionCall: ({ methodName, gas, deposit, args, argsBase64 }) => ({
470
+ type: "FunctionCall",
471
+ methodName,
472
+ args,
473
+ argsBase64,
474
+ gas,
475
+ deposit,
476
+ }),
477
+
478
+ transfer: (yoctoAmount) => ({
479
+ type: "Transfer",
480
+ deposit: yoctoAmount,
481
+ }),
482
+
483
+ stakeNEAR: ({ amount, publicKey }) => ({
484
+ type: "Stake",
485
+ stake: amount,
486
+ publicKey,
487
+ }),
488
+
489
+ addFullAccessKey: ({ publicKey }) => ({
490
+ type: "AddKey",
491
+ publicKey: publicKey,
492
+ accessKey: { permission: "FullAccess" },
493
+ }),
494
+
495
+ addLimitedAccessKey: ({
496
+ publicKey,
497
+ allowance,
498
+ accountId,
499
+ methodNames,
500
+ }) => ({
501
+ type: "AddKey",
502
+ publicKey: publicKey,
503
+ accessKey: {
504
+ permission: "FunctionCall",
505
+ allowance,
506
+ receiverId: accountId,
507
+ methodNames,
508
+ },
509
+ }),
510
+
511
+ deleteKey: ({ publicKey }) => ({
512
+ type: "DeleteKey",
513
+ publicKey,
514
+ }),
515
+
516
+ deleteAccount: ({ beneficiaryId }) => ({
517
+ type: "DeleteAccount",
518
+ beneficiaryId,
519
+ }),
520
+
521
+ createAccount: () => ({
522
+ type: "CreateAccount",
523
+ }),
524
+
525
+ deployContract: ({ codeBase64 }) => ({
526
+ type: "DeployContract",
527
+ codeBase64,
528
+ }),
529
+ },
530
+ };
531
+
532
+ // Handle wallet redirect if applicable
533
+ // TODO: Implement actual wallet integration
534
+ try {
535
+ const url = new URL(window.location.href);
536
+ const accountId = url.searchParams.get("account_id");
537
+ const publicKey = url.searchParams.get("public_key");
538
+ const errorCode = url.searchParams.get("error_code");
539
+
540
+ if (errorCode) {
541
+ console.error(new Error(`Wallet error: ${errorCode}`));
542
+ }
543
+
544
+ if (accountId && publicKey) {
545
+ if (publicKey === _state.publicKey) {
546
+ updateState({
547
+ accountId,
548
+ });
549
+ } else {
550
+ console.error(
551
+ new Error("Public key mismatch from wallet redirect"),
552
+ publicKey,
553
+ _state.publicKey,
554
+ );
555
+ }
556
+ }
557
+ // Remove wallet parameters from the URL
558
+ url.searchParams.delete("account_id");
559
+ url.searchParams.delete("public_key");
560
+ url.searchParams.delete("error_code");
561
+ url.searchParams.delete("all_keys");
562
+ window.history.replaceState({}, "", url.toString());
563
+ } catch (e) {
564
+ console.error("Error handling wallet redirect:", e);
565
+ }
566
+
567
+ export { api, convertUnit };