@mentaproject/client 0.1.15 → 0.1.17

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.
@@ -26,7 +26,8 @@ export declare class PersistenceManager {
26
26
  * Synchronizes an account's transactions from the remote source to the local persistence.
27
27
  * It fetches new transactions starting from the last synced block and updates the local storage.
28
28
  * @param account - The account whose transactions are to be synchronized.
29
+ * @param limit - The maximum number of transactions to fetch.
29
30
  * @returns A promise that resolves when the synchronization is complete.
30
31
  */
31
- syncTransactions(account: Account, toBlock?: bigint): Promise<void>;
32
+ syncTransactions(account: Account, limit?: number): Promise<void>;
32
33
  }
@@ -27,16 +27,18 @@ export class PersistenceManager {
27
27
  * Synchronizes an account's transactions from the remote source to the local persistence.
28
28
  * It fetches new transactions starting from the last synced block and updates the local storage.
29
29
  * @param account - The account whose transactions are to be synchronized.
30
+ * @param limit - The maximum number of transactions to fetch.
30
31
  * @returns A promise that resolves when the synchronization is complete.
31
32
  */
32
- async syncTransactions(account, toBlock) {
33
- const lastBlock = toBlock ?? (await getBlockNumber(this.client.rpc));
33
+ async syncTransactions(account, limit = 100000) {
34
34
  const lastSyncedBlock = await this.persistenceAdapter.getLastSyncedBlock(account.address);
35
- const fromBlock = lastSyncedBlock ? lastSyncedBlock + 1n : 0n;
35
+ const lastBlock = lastSyncedBlock ?? 0n;
36
+ const fromBlock = lastSyncedBlock ? lastSyncedBlock + 1n : (await getBlockNumber(this.client.rpc));
36
37
  const newTransactions = await account.transactions({
37
38
  fromBlock: fromBlock,
38
- toBlock: lastBlock
39
- });
39
+ toBlock: lastBlock,
40
+ limit: limit
41
+ }, true);
40
42
  if (newTransactions.length > 0) {
41
43
  await this.persistenceAdapter.upsertTransactions(newTransactions);
42
44
  const latestBlock = bigIntMax(...newTransactions.map(t => t.blockNumber || 0n));
@@ -65,18 +65,19 @@ export declare class Account implements AccountData {
65
65
  * @param {number} [options.page] - The page number for paginated results.
66
66
  * @param {number} [options.offset] - The number of items to skip from the beginning of the result set.
67
67
  * @param {'asc' | 'desc'} [options.sort] - The sorting order for transactions based on block number ('asc' for ascending, 'desc' for descending).
68
+ * @param {boolean} [forceFetch=false] - Forces the method to fetch transactions from the remote source even if they are already cached. (mostly used internally)
68
69
  * @returns {Promise<Transaction[]>} A promise that resolves to an array of transactions.
69
70
  */
70
- transactions(params: GetTransactionsParams): Promise<Transaction[]>;
71
+ transactions(params: GetTransactionsParams, forceFetch?: boolean): Promise<Transaction[]>;
71
72
  /**
72
73
  * Synchronizes the account's transactions with the remote data source using the configured `PersistenceManager`.
73
74
  * @description This method initiates a synchronization process to ensure that the local cache of transactions
74
75
  * for this account is up-to-date with the blockchain.
75
- * @param {bigint} [toBlock] The block number up to which to synchronize transactions.
76
+ * @param {number} [limit] The maximum number of transactions to synchronize.
76
77
  * @returns {Promise<void>} A promise that resolves when the synchronization process is complete.
77
78
  * @throws {Error} If the persistence module is not configured.
78
79
  */
79
- syncTransactions(toBlock?: bigint): Promise<void>;
80
+ syncTransactions(limit?: number): Promise<void>;
80
81
  /**
81
82
  * Retrieves transactions involving a specific token. (not implemented yet)
82
83
  * @description This method queries the persistence adapter to retrieve transactions involving a specific token.
@@ -92,10 +92,11 @@ export class Account {
92
92
  * @param {number} [options.page] - The page number for paginated results.
93
93
  * @param {number} [options.offset] - The number of items to skip from the beginning of the result set.
94
94
  * @param {'asc' | 'desc'} [options.sort] - The sorting order for transactions based on block number ('asc' for ascending, 'desc' for descending).
95
+ * @param {boolean} [forceFetch=false] - Forces the method to fetch transactions from the remote source even if they are already cached. (mostly used internally)
95
96
  * @returns {Promise<Transaction[]>} A promise that resolves to an array of transactions.
96
97
  */
97
- async transactions(params) {
98
- if (!this.persistenceManager) {
98
+ async transactions(params, forceFetch = false) {
99
+ if (!this.persistenceManager || forceFetch) {
99
100
  // If persistence is not configured, fetch directly from remote
100
101
  const hashes = await this._fetchTransactions(params);
101
102
  const transactions = await Promise.all(hashes.map(hash => this.client.transactions.get({ hash })));
@@ -108,14 +109,14 @@ export class Account {
108
109
  * Synchronizes the account's transactions with the remote data source using the configured `PersistenceManager`.
109
110
  * @description This method initiates a synchronization process to ensure that the local cache of transactions
110
111
  * for this account is up-to-date with the blockchain.
111
- * @param {bigint} [toBlock] The block number up to which to synchronize transactions.
112
+ * @param {number} [limit] The maximum number of transactions to synchronize.
112
113
  * @returns {Promise<void>} A promise that resolves when the synchronization process is complete.
113
114
  * @throws {Error} If the persistence module is not configured.
114
115
  */
115
- async syncTransactions(toBlock) {
116
+ async syncTransactions(limit = 1000) {
116
117
  if (!this.persistenceManager)
117
118
  throw new Error("The persistence module is not configured.");
118
- await this.persistenceManager.syncTransactions(this, toBlock);
119
+ await this.persistenceManager.syncTransactions(this, limit);
119
120
  }
120
121
  /**
121
122
  * Retrieves transactions involving a specific token. (not implemented yet)
@@ -139,11 +140,11 @@ export class Account {
139
140
  async toJSON(depth = 1) {
140
141
  return await toJSON({
141
142
  obj: {
142
- ...this,
143
- isContract: this.isContract,
144
- contractType: this.contractType,
145
- ETHBalance: this.ETHBalance,
146
- transactionCount: this.transactionCount,
143
+ address: this.address,
144
+ isContract: this.isContract.bind(this),
145
+ contractType: this.contractType.bind(this),
146
+ ETHBalance: this.ETHBalance.bind(this),
147
+ transactionCount: this.transactionCount.bind(this),
147
148
  },
148
149
  depth
149
150
  });
@@ -167,12 +168,8 @@ export class Account {
167
168
  toBlock: toHex(toBlock),
168
169
  toAddress: this.address
169
170
  });
170
- const traces = outgoing.concat(incoming);
171
- return traces.map(t => ({
172
- blockNumber: BigInt(t.blockNumber),
173
- index: t.transactionPosition,
174
- hash: t.transactionHash
175
- }));
171
+ const traces = outgoing.concat(incoming).sort((a, b) => a.blockNumber - b.blockNumber);
172
+ return traces.map(t => t.transactionHash);
176
173
  };
177
174
  return await fetchByBlockRange({
178
175
  toBlock: BigInt(toBlock !== undefined ? toBlock : 0),
@@ -33,5 +33,6 @@ export type AccountData = {
33
33
  export type JSONAccount = AccountData & {
34
34
  isContract: boolean;
35
35
  ETHBalance: string;
36
+ contractType: string;
36
37
  transactionCount: number;
37
38
  };
@@ -4,6 +4,7 @@
4
4
  import { Hex, TransactionReceipt, Transaction as RawTransaction } from "@mentaproject/core";
5
5
  import { Account } from "../structures";
6
6
  import { BlockData } from "./Block";
7
+ import { JSONAccount } from "./Account";
7
8
  /**
8
9
  * Defines the direction of a transaction relative to an account.
9
10
  * "in" for incoming transactions, "out" for outgoing transactions.
@@ -32,6 +33,8 @@ export type TransactionCall = {
32
33
  callType: string;
33
34
  };
34
35
  export type JSONTransaction = RawTransaction & {
36
+ from: JSONAccount;
37
+ to?: JSONAccount;
35
38
  block: BlockData;
36
39
  receipt: TransactionReceipt;
37
40
  calls?: TransactionCall[];
@@ -23,7 +23,7 @@ export function withCache(client, cache) {
23
23
  // 3. We replace the client's `request` method
24
24
  client.request = (async (args) => {
25
25
  const { method, params } = args;
26
- const cacheKey = `${client.chain.id}:${method}:${JSON.stringify(params || [])}`;
26
+ const cacheKey = `${client.chain.id}:${method}:${JSON.stringify(params || [], (_, v) => typeof v === 'bigint' ? v.toString() : v)}`;
27
27
  let result;
28
28
  if (method !== "eth_blockNumber")
29
29
  result = cache.get(cacheKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentaproject/client",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "High level EVM library used into the Menta App to facilitate Blockchain interactions. ",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -32,7 +32,7 @@
32
32
  "license": "ISC",
33
33
  "dependencies": {
34
34
  "@mentaproject/contracts": "^0.0.9",
35
- "@mentaproject/core": "^0.5.2",
35
+ "@mentaproject/core": "^0.5.3",
36
36
  "@modelcontextprotocol/sdk": "^1.13.0",
37
37
  "@shazow/whatsabi": "^0.21.1"
38
38
  },
package/test.ts CHANGED
@@ -1,24 +1,28 @@
1
- import { http } from "@mentaproject/core";
2
- import { MentaClient } from "./src";
1
+ import { Address, checksumAddress, http } from "@mentaproject/core";
2
+ import { MentaClient, Transaction } from "./src";
3
3
  import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"
4
4
  import { mainnet } from "@mentaproject/core/chains";
5
5
  import { MemoryCache } from "./src/structures/Cache";
6
6
  import { watchBlockNumber, watchBlocks } from "@mentaproject/core/actions";
7
7
  import { writeFileSync } from "fs";
8
+ import { JSONTransaction } from "./src/types";
8
9
 
9
10
  const viemAccount = privateKeyToAccount(generatePrivateKey());
10
11
 
11
12
  let counter = 0;
13
+ let lastSyncedBlock = new Map<Address, bigint>();
14
+ let transactions = new Map<Address, JSONTransaction[]>();
15
+
12
16
  const client = new MentaClient({
13
17
  account: viemAccount,
14
18
  transport: http("https://ethereum-rpc.publicnode.com", {
15
19
  batch: {
16
- batchSize: 5,
17
- wait: 500,
20
+ batchSize: 50,
21
+ wait: 20,
18
22
  },
19
23
  onFetchRequest: async (r) => {
20
- const data = await r.json();
21
- console.log(data);
24
+ // const data = await r.json();
25
+ // console.log(data.map(j => `${j.method}(${j.params?.map(p => JSON.stringify(p)).join(', ')})`).join('\n'));
22
26
  // console.log(`>> ${counter} requests`);
23
27
 
24
28
  // console.log(`>> ${r.url}\n>> ${req.map(j => j.method).join(', ')}\n>> ${delay / 1000}s\n`)
@@ -26,14 +30,55 @@ const client = new MentaClient({
26
30
  // onFetchResponse: (r) => console.log(`<<: ${r.url}`),
27
31
  }),
28
32
  cache: new MemoryCache(0n, { maxSize: 1000, defaultTtl: 1 }),
33
+ persistenceAdapter: {
34
+ getLastSyncedBlock: async (accountAddress: Address) => {
35
+ return lastSyncedBlock.get(accountAddress) ?? null;
36
+ },
37
+ getTransactions: async (address: Address, params) => {
38
+ return transactions.get(address) ?? [];
39
+ },
40
+ upsertTransactions: async (txs: Transaction[]) => {
41
+ console.log("upsertTransactions", txs.length);
42
+ for (const transaction of txs) {
43
+ const fromAddress = transaction.from?.address;
44
+ const toAddress = transaction.to?.address;
45
+
46
+ if (fromAddress) {
47
+ const checksumed = checksumAddress(fromAddress);
48
+
49
+ if (!transactions.has(checksumed)) {
50
+ transactions.set(checksumed, []);
51
+ };
52
+
53
+ const jsonTransaction = await transaction.toJSON();
54
+ transactions.get(checksumed)?.push(jsonTransaction);
55
+ };
56
+
57
+ if (toAddress) {
58
+ const checksumed = checksumAddress(toAddress);
59
+
60
+ if (!transactions.has(checksumed)) {
61
+ transactions.set(checksumed, []);
62
+ };
63
+
64
+ const jsonTransaction = await transaction.toJSON();
65
+ transactions.get(checksumed)?.push(jsonTransaction);
66
+ }
67
+ }
68
+ },
69
+ setLastSyncedBlock: async (accountAddress: Address, blockNumber: bigint) => {
70
+ lastSyncedBlock.set(accountAddress, blockNumber);
71
+ },
72
+ },
29
73
  chain: mainnet
30
74
  });
31
75
 
32
76
  (async () => {
33
77
  console.log("Watching for new blocks and block numbers...");
34
78
 
35
- const item = await client.transactions.get({ hash: "0x2ac872604f7ce5e132417b881bfa80a66836ca669c0720e8ce2cc40c9c92a696" });
36
- const json = await item.toJSON(1);
79
+ const account = client.accounts.get("0x53b9B72DC6f96Eb4B54143B211B22e2548e4cf5c");
80
+ await account.syncTransactions(20)
81
+ const res = await account.transactions({ limit: 20 });
37
82
 
38
- writeFileSync("./test.json", JSON.stringify(json, null, 2));
83
+ console.log(res.length);
39
84
  })();
@@ -8,6 +8,7 @@ import { Address, Hash } from '@mentaproject/core/types';
8
8
 
9
9
  // Mocking the core actions
10
10
  jest.mock('@mentaproject/core/actions', () => ({
11
+ getBlock: jest.fn(),
11
12
  getCode: jest.fn(),
12
13
  getContractType: jest.fn(),
13
14
  sendTransaction: jest.fn(),
@@ -24,26 +25,27 @@ jest.mock('@mentaproject/contracts', () => ({
24
25
  getContractType: jest.fn(),
25
26
  }));
26
27
 
27
- // Mocking the MentaClient and its dependencies
28
- const mockMentaClient = {
29
- rpc: {},
30
- transactions: {
31
- parse: jest.fn((data) => new Transaction(mockMentaClient as any, data)),
32
- get: jest.fn(),
33
- },
34
- } as unknown as MentaClient;
28
+ jest.mock('../../src/structures/MentaClient');
29
+ jest.mock('../../src/managers/PersistenceManager');
35
30
 
36
- const mockPersistenceManager = {
37
- getTransactions: jest.fn(),
38
- syncTransactions: jest.fn(),
39
- } as unknown as PersistenceManager;
31
+ const MockedMentaClient = MentaClient as jest.MockedClass<typeof MentaClient>;
32
+ const MockedPersistenceManager = PersistenceManager as jest.MockedClass<typeof PersistenceManager>;
40
33
 
41
34
  describe('Account', () => {
42
35
  const mockAddress: Address = '0x1234567890123456789012345678901234567890';
43
36
  let account: Account;
37
+ let mockMentaClient: jest.Mocked<MentaClient>;
38
+ let mockPersistenceManager: jest.Mocked<PersistenceManager>;
44
39
 
45
40
  beforeEach(() => {
46
41
  jest.clearAllMocks();
42
+ mockMentaClient = new MockedMentaClient({} as any) as jest.Mocked<MentaClient>;
43
+ mockMentaClient.transactions = {
44
+ get: jest.fn(),
45
+ parse: jest.fn().mockImplementation((data) => new Transaction(mockMentaClient, data)),
46
+ send: jest.fn(),
47
+ } as any;
48
+ mockPersistenceManager = new MockedPersistenceManager(mockMentaClient, {} as any) as jest.Mocked<PersistenceManager>;
47
49
  account = new Account(mockMentaClient, mockAddress, mockPersistenceManager);
48
50
  });
49
51
 
@@ -82,8 +84,28 @@ describe('Account', () => {
82
84
 
83
85
  describe('sendETH', () => {
84
86
  it('should send ETH and return a Transaction object', async () => {
85
- const mockTxHash: Hash = '0x987654321';
86
- const mockTxData = { hash: mockTxHash, from: mockAddress, to: mockAddress, value: 1000n };
87
+ const mockTxHash: Hash = '0x98765432109876543210987654321098765432109876543210987654321098765';
88
+ const mockTxData: import('@mentaproject/core/types').Transaction = {
89
+ blockHash: '0xblockhash',
90
+ blockNumber: 123n,
91
+ from: mockAddress,
92
+ gas: 21000n,
93
+ gasPrice: 1000000000n,
94
+ hash: mockTxHash,
95
+ input: '0x',
96
+ nonce: 1,
97
+ r: '0xr',
98
+ s: '0xs',
99
+ to: mockAddress,
100
+ transactionIndex: 0,
101
+ type: 'legacy',
102
+ v: 27n,
103
+ value: 1000n,
104
+ accessList: undefined,
105
+ typeHex: null,
106
+ };
107
+
108
+ (coreActions.sendTransaction as jest.Mock).mockResolvedValue(mockTxHash);
87
109
 
88
110
  (coreActions.sendTransaction as jest.Mock).mockResolvedValue(mockTxHash);
89
111
  (coreActions.getTransaction as jest.Mock).mockResolvedValue(mockTxData);
@@ -120,27 +142,45 @@ describe('Account', () => {
120
142
 
121
143
  describe('transactions', () => {
122
144
  it('should fetch transactions from persistence manager if available', async () => {
123
- const mockTransactions = [new Transaction(mockMentaClient, { hash: '0x1' } as any)];
124
- (mockPersistenceManager.getTransactions as jest.Mock).mockResolvedValue(mockTransactions);
145
+ const mockTxJSONs = [{ hash: '0x1' }];
146
+ (mockPersistenceManager.getTransactions as jest.Mock).mockResolvedValue(mockTxJSONs as any);
125
147
 
126
148
  const result = await account.transactions({});
127
149
  expect(mockPersistenceManager.getTransactions).toHaveBeenCalledWith(mockAddress, {});
128
- expect(result).toEqual(mockTransactions);
150
+ expect(result.map(tx => tx.hash)).toEqual(mockTxJSONs.map(tx => tx.hash));
129
151
  });
130
152
 
131
153
  it('should fetch transactions from remote if persistence is not configured', async () => {
132
154
  const localAccount = new Account(mockMentaClient, mockAddress); // No persistenceManager
133
155
  const mockHashes: Hash[] = ['0x1', '0x2'];
134
- const mockTx1 = new Transaction(mockMentaClient, { hash: '0x1' } as any);
135
- const mockTx2 = new Transaction(mockMentaClient, { hash: '0x2' } as any);
156
+ const mockBaseTx = {
157
+ blockHash: '0xblockhash',
158
+ blockNumber: 123n,
159
+ from: mockAddress,
160
+ gas: 21000n,
161
+ gasPrice: 1000000000n,
162
+ input: '0x',
163
+ nonce: 1,
164
+ r: '0xr',
165
+ s: '0xs',
166
+ to: mockAddress,
167
+ transactionIndex: 0,
168
+ type: 'legacy',
169
+ v: 27n,
170
+ value: 1000n,
171
+ accessList: undefined,
172
+ typeHex: null,
173
+ };
174
+ const mockTx1 = new Transaction(mockMentaClient, { ...mockBaseTx, hash: '0x1' } as any);
175
+ const mockTx2 = new Transaction(mockMentaClient, { ...mockBaseTx, hash: '0x2' } as any);
136
176
 
137
177
  jest.spyOn(localAccount as any, '_fetchTransactions').mockResolvedValue(mockHashes);
138
- (mockMentaClient.transactions.get as jest.Mock).mockImplementation(({ hash }) => {
139
- if (hash === '0x1') return Promise.resolve(mockTx1);
140
- if (hash === '0x2') return Promise.resolve(mockTx2);
141
- return Promise.resolve(null);
178
+ (mockMentaClient.transactions.get as jest.Mock).mockImplementation(async ({ hash }) => {
179
+ if (hash === '0x1') return mockTx1;
180
+ if (hash === '0x2') return mockTx2;
181
+ return null as any;
142
182
  });
143
-
183
+
144
184
  const result = await localAccount.transactions({});
145
185
  expect(localAccount['_fetchTransactions']).toHaveBeenCalled();
146
186
  expect(mockMentaClient.transactions.get).toHaveBeenCalledTimes(2);
@@ -150,8 +190,8 @@ describe('Account', () => {
150
190
 
151
191
  describe('syncTransactions', () => {
152
192
  it('should call syncTransactions on the persistence manager', async () => {
153
- await account.syncTransactions(1n);
154
- expect(mockPersistenceManager.syncTransactions).toHaveBeenCalledWith(account, 1n);
193
+ await account.syncTransactions(1);
194
+ expect(mockPersistenceManager.syncTransactions).toHaveBeenCalledWith(account, 1);
155
195
  });
156
196
 
157
197
  it('should throw an error if persistence manager is not configured', async () => {
@@ -2,8 +2,8 @@ import { Block } from '../../src/structures/Block';
2
2
  import { MentaClient } from '../../src/structures/MentaClient';
3
3
  import { Account } from '../../src/structures/Account';
4
4
  import { Transaction } from '../../src/structures/Transaction';
5
- import { IBlockData } from '../../src/types/Block';
6
5
  import { Address, Hash } from '@mentaproject/core/types';
6
+ import { BlockData } from '../../src/types';
7
7
 
8
8
  // Mocking MentaClient
9
9
  const mockMentaClient = {
@@ -21,7 +21,7 @@ describe('Block', () => {
21
21
  const mockMinerAddress: Address = '0xminer123';
22
22
  const mockTxHash: Hash = '0xhash123';
23
23
 
24
- const mockBlockData: IBlockData = {
24
+ const mockBlockData: BlockData = {
25
25
  number: 123n,
26
26
  hash: '0xblockhash',
27
27
  parentHash: '0xparenthash',
@@ -3,7 +3,7 @@ import { MentaClient } from '../../src/structures/MentaClient';
3
3
  import { Account } from '../../src/structures/Account';
4
4
  import { Block } from '../../src/structures/Block';
5
5
  import * as coreActions from '@mentaproject/core/actions';
6
- import { RawTransaction, Hash, Address } from '@mentaproject/core/types';
6
+ import { Transaction as RawTransaction, Hash, Address } from '@mentaproject/core/types';
7
7
 
8
8
  // Mocking core actions
9
9
  jest.mock('@mentaproject/core/actions', () => ({
@@ -83,8 +83,8 @@ describe('Transaction', () => {
83
83
 
84
84
  expect(coreActions.traceTransaction).toHaveBeenCalledWith(mockMentaClient.rpc, mockTxHash);
85
85
  expect(calls).toHaveLength(1);
86
- expect(calls[0].value).toBe(100n);
87
- expect(calls[0].from).toBeInstanceOf(Account);
86
+ expect(calls![0].value).toBe(100n);
87
+ expect(calls![0].from).toBeInstanceOf(Account);
88
88
  });
89
89
  });
90
90
 
@@ -63,7 +63,7 @@ describe('toJSON', () => {
63
63
  a: 1,
64
64
  toJSON: () => ({}),
65
65
  constructor: () => {},
66
- rpcClient: {},
66
+ client: {},
67
67
  };
68
68
  const json = await toJSON({ obj });
69
69
  expect(json).toEqual({ a: 1 });