diff --git a/__tests__/config/fixtures.ts b/__tests__/config/fixtures.ts index 0443acd5d..0d49004a9 100644 --- a/__tests__/config/fixtures.ts +++ b/__tests__/config/fixtures.ts @@ -2,7 +2,12 @@ import fs from 'node:fs'; import path from 'node:path'; import { Account, Provider, ProviderInterface, RpcProvider, json } from '../../src'; -import { CompiledSierra, CompiledSierraCasm, LegacyCompiledContract } from '../../src/types'; +import { + CompiledSierra, + CompiledSierraCasm, + LegacyCompiledContract, + RpcProviderOptions, +} from '../../src/types'; import { ETransactionVersion } from '../../src/types/api'; import { toHex } from '../../src/utils/num'; import { wait } from '../../src/utils/provider'; @@ -72,12 +77,22 @@ export const compiledSidMulticallCasm = readContractSierraCasm('starknetId/multi export const compiledNonZero = readContractSierra('cairo/cairo263/zeroable.sierra'); export const compiledNonZeroCasm = readContractSierraCasm('cairo/cairo263/zeroable'); -export function getTestProvider(isProvider?: true): ProviderInterface; -export function getTestProvider(isProvider?: false): RpcProvider; -export function getTestProvider(isProvider: boolean = true): ProviderInterface | RpcProvider { +export function getTestProvider( + isProvider?: true, + setProviderOptions?: RpcProviderOptions +): ProviderInterface; +export function getTestProvider( + isProvider?: false, + setProviderOptions?: RpcProviderOptions +): RpcProvider; +export function getTestProvider( + isProvider: boolean = true, + setProviderOptions?: RpcProviderOptions +): ProviderInterface | RpcProvider { const isDevnet = process.env.IS_DEVNET === 'true'; - const providerOptions = { + const providerOptions: RpcProviderOptions = { + ...setProviderOptions, nodeUrl: process.env.TEST_RPC_URL, // accelerate the tests when running locally ...(isDevnet && { transactionRetryIntervalFallback: 1000 }), diff --git a/__tests__/config/jest.setup.ts b/__tests__/config/jest.setup.ts index 90bbc1b5b..21078680d 100644 --- a/__tests__/config/jest.setup.ts +++ b/__tests__/config/jest.setup.ts @@ -18,31 +18,46 @@ const combiner: object[] = []; if (process.env.DEBUG === 'true') { register({ request(url, config) { - const body = JSON.parse(config.body); - combiner.push({ - request: { - url, - method: config.method, - body, - }, - }); + const randId = crypto.randomUUID(); + if (config.body) { + const body = JSON.parse(config.body); + combiner.push({ + request: { + matchId: randId, + url, + method: config.method, + body, + }, + }); + + // match request and response when DEBUG, lib override headers instead of add + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'x-match-id': randId, + }; + // eslint-disable-next-line no-param-reassign + config.headers = headers; + } return [url, config]; }, requestError(error) { - const match: any = combiner.find((it: any) => typeof it.result === 'undefined'); - match.result = error; - console.log('[fetch.requestError]', match); + // unknown original request + console.log('[fetch.requestError]', error); return Promise.reject(error); }, response(response) { + const requestId = response.request.headers.get('x-match-id'); const cloned = response.clone(); cloned.json().then((res) => { const { result } = res; - const match: any = combiner.find((it: any) => it.request.body.id === res.id); + const match: any = combiner.find((it: any) => it.request.matchId === requestId); if (match && 'request' in match) { - match.result = result; + if (result) match.result = result; + else match.response = res; + console.log(util.inspect(match, false, null, true /* enable colors */)); } else { console.log(result); @@ -52,9 +67,8 @@ if (process.env.DEBUG === 'true') { }, responseError(error) { - const match: any = combiner.find((it: any) => typeof it.result === 'undefined'); - match.result = error; - console.log('[fetch.responseError]', match); + // unknown original request + console.log('[fetch.responseError]', error); return Promise.reject(error); }, }); diff --git a/__tests__/utils/batch.test.ts b/__tests__/utils/batch.test.ts new file mode 100644 index 000000000..8c878290e --- /dev/null +++ b/__tests__/utils/batch.test.ts @@ -0,0 +1,49 @@ +import { BatchClient } from '../../src/utils/batch'; +import { createBlockForDevnet, getTestProvider } from '../config/fixtures'; +import { initializeMatcher } from '../config/schema'; + +describe('Batch Client', () => { + const provider = getTestProvider(false); + + const batchClient = new BatchClient({ + nodeUrl: provider.channel.nodeUrl, + headers: provider.channel.headers, + interval: 0, + }); + + initializeMatcher(expect); + + test('should batch two requests', async () => { + await createBlockForDevnet(); + + const fetchSpy = jest.spyOn(batchClient as any, 'sendBatch'); + + const [blockNumber, blockWithReceipts] = await Promise.all([ + batchClient.fetch('starknet_blockNumber'), + batchClient.fetch('starknet_getBlockWithReceipts', { block_id: 'latest' }), + ]); + + expect(typeof blockNumber.result).toBe('number'); + expect(blockWithReceipts.result).toMatchSchemaRef('BlockWithTxReceipts'); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + fetchSpy.mockRestore(); + }); + + test('batch request using Provider', async () => { + const myBatchProvider = getTestProvider(false, { + batch: 0, + }); + + // eslint-disable-next-line @typescript-eslint/dot-notation + const sendBatchSpy = jest.spyOn(myBatchProvider.channel['batchClient'] as any, 'sendBatch'); + + await Promise.all([ + myBatchProvider.getBlock(), + myBatchProvider.getBlockLatestAccepted(), + myBatchProvider.getBlockTransactionCount('latest'), + ]); + + expect(sendBatchSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/__tests__/utils/encode.test.ts b/__tests__/utils/encode.test.ts index c0d3df623..a3efb032a 100644 --- a/__tests__/utils/encode.test.ts +++ b/__tests__/utils/encode.test.ts @@ -1,3 +1,4 @@ +import { encode } from '../../src'; import { atobUniversal, btoaUniversal } from '../../src/utils/encode'; describe('atobUniversal and btoaUniversal functions', () => { @@ -32,3 +33,12 @@ describe('atobUniversal and btoaUniversal functions', () => { expect(decoded).toEqual(new Uint8Array([])); }); }); + +describe('concatenateArrayBuffer', () => { + test('should concatenate uint8Arrays', () => { + const path0buff = new Uint8Array([128, 0, 10, 85]); + const path1buff = new Uint8Array([71, 65, 233, 201]); + const result = encode.concatenateArrayBuffer([path0buff, path1buff]); + expect(result).toEqual(new Uint8Array([128, 0, 10, 85, 71, 65, 233, 201])); + }); +}); diff --git a/__tests__/utils/ethSigner.test.ts b/__tests__/utils/ethSigner.test.ts index 37d4a684d..53c0370ce 100644 --- a/__tests__/utils/ethSigner.test.ts +++ b/__tests__/utils/ethSigner.test.ts @@ -10,6 +10,7 @@ import { encode, eth, extractContractHashes, + getLedgerPathBuffer, hash, num, stark, @@ -353,3 +354,16 @@ describe('Ethereum signer', () => { }); }); }); + +describe('Ledger Signer', () => { + // signature of Ledger can't be tested automatically. + // So, just the test of the path encoding. + test('getLedgerPathBuffer', () => { + const path = getLedgerPathBuffer(3, 'AstroAPP'); + expect(path).toEqual( + new Uint8Array([ + 128, 0, 10, 85, 71, 65, 233, 201, 95, 192, 123, 107, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, + ]) + ); + }); +}); diff --git a/__tests__/utils/hash.test.ts b/__tests__/utils/hash.test.ts index 2d1399794..2b24789f3 100644 --- a/__tests__/utils/hash.test.ts +++ b/__tests__/utils/hash.test.ts @@ -1,4 +1,5 @@ import { keccakBn, starknetKeccak, getSelectorFromName, getSelector } from '../../src/utils/hash'; +import { constants, hash } from '../../src'; describe('keccakBn', () => { test('should properly calculate the Keccak hash', () => { @@ -38,4 +39,62 @@ describe('getSelector', () => { test('should return the proper selector when provided a decimal string', () => { expect(getSelector('123456')).toBe('0x1e240'); }); + + test('should return the proper selector when provided a number', () => { + expect(getSelector(123456)).toBe('0x1e240'); + }); + + test('should return the proper selector when provided a bigint', () => { + expect(getSelector(123456n)).toBe('0x1e240'); + }); +}); + +describe('L1->L2 messaging', () => { + // L1 tx for a message L1->L2 + // data extracted from : + // https://sepolia.etherscan.io/tx/0xd82ce7dd9f3964d89d2eb9d555e1460fb7792be274950abe578d610f95cc40f5 + // data extracted from etherscan : + const l1FromAddress = '0x0000000000000000000000008453fc6cd1bcfe8d4dfc069c400b433054d47bdc'; + const l2ToAddress = 2158142789748719025684046545159279785659305214176670733242887773692203401023n; + const l2Selector = 774397379524139446221206168840917193112228400237242521560346153613428128537n; + const payload = [ + 4543560n, + 829565602143178078434185452406102222830667255948n, + 3461886633118033953192540141609307739580461579986333346825796013261542798665n, + 9000000000000000n, + 0n, + ]; + const l1Nonce = 8288n; + + test('solidityUint256PackedKeccak256', () => { + const kec256Hash = hash.solidityUint256PackedKeccak256(['0x100', '200', 300, 400n]); + expect(kec256Hash).toBe('0xd1e6cb422b65269603c491b0c85463295edabebfb2a6844e4fdc389ff1dcdd97'); + }); + + test('getL2MessageHash', () => { + // https://sepolia.starkscan.co/message/0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090#messagelogs + const l1ToL2MessageHash = hash.getL2MessageHash( + l1FromAddress, + l2ToAddress, + l2Selector, + payload, + l1Nonce + ); + expect(l1ToL2MessageHash).toBe( + '0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090' + ); + }); + + test('calculateL2MessageTxHash', () => { + // https://sepolia.starkscan.co/tx/0x067d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07 + const l2TxHash = hash.calculateL2MessageTxHash( + l1FromAddress, + l2ToAddress, + l2Selector, + payload, + constants.StarknetChainId.SN_SEPOLIA, + l1Nonce + ); + expect(l2TxHash).toBe('0x67d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07'); + }); }); diff --git a/__tests__/utils/num.test.ts b/__tests__/utils/num.test.ts index 04e7ab33b..78b08d194 100644 --- a/__tests__/utils/num.test.ts +++ b/__tests__/utils/num.test.ts @@ -18,6 +18,7 @@ import { isNumber, isBoolean, } from '../../src/utils/num'; +import { num } from '../../src'; describe('isHex', () => { test('should return true for valid hex strings', () => { @@ -208,3 +209,10 @@ describe('isBoolean', () => { expect(isBoolean({})).toBe(false); }); }); + +describe('stringToSha256ToArrayBuff4', () => { + test('should correctly hash&encode an utf8 string', () => { + const buff = num.stringToSha256ToArrayBuff4('LedgerW'); + expect(buff).toEqual(new Uint8Array([43, 206, 231, 219])); + }); +}); diff --git a/package-lock.json b/package-lock.json index d55aabfe7..9b4fdbd5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { - "name": "@penovicp/starknet", - "version": "7.0.0", + "name": "starknet", + "version": "6.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@penovicp/starknet", - "version": "7.0.0", + "name": "starknet", + "version": "6.11.0", "license": "MIT", "dependencies": { + "@ledgerhq/hw-transport": "^6.31.1", "@noble/curves": "~1.4.0", "@noble/hashes": "^1.4.0", "@scure/base": "~1.1.3", @@ -3858,6 +3859,49 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@ledgerhq/devices": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.4.1.tgz", + "integrity": "sha512-Mbjzqlcj4Q2StxEmaYEb5wv6sK5Sk26L4xs0BC9io/AyvpXNTDAp67tryB/klNcvd+WwZPcPdYYvlNzfQ0WTUA==", + "dependencies": { + "@ledgerhq/errors": "^6.18.0", + "@ledgerhq/logs": "^6.12.0", + "rxjs": "^7.8.1", + "semver": "^7.3.5" + } + }, + "node_modules/@ledgerhq/devices/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ledgerhq/errors": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.18.0.tgz", + "integrity": "sha512-L3jQWAGyooxRDk/MRlW2v4Ji9+kloBtdmz9wBkHaj2j0n+05rweJSV3GHw9oye1BYMbVFqFffmT4H3hlXlCasw==" + }, + "node_modules/@ledgerhq/hw-transport": { + "version": "6.31.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.31.1.tgz", + "integrity": "sha512-0hVcrqUOM7AYV/JEq8yoeBiXLjpWrentgYt8MC3n+iNFfpORU/SUprcbu0s884IHzj+a8mx0JCZp9y7uPSLlzg==", + "dependencies": { + "@ledgerhq/devices": "^8.4.1", + "@ledgerhq/errors": "^6.18.0", + "@ledgerhq/logs": "^6.12.0", + "events": "^3.3.0" + } + }, + "node_modules/@ledgerhq/logs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.12.0.tgz", + "integrity": "sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA==" + }, "node_modules/@noble/curves": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", @@ -8731,6 +8775,14 @@ "dev": true, "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -13938,6 +13990,8 @@ }, "node_modules/npm/node_modules/@isaacs/cliui": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "inBundle": true, "license": "ISC", @@ -13955,6 +14009,8 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, "inBundle": true, "license": "MIT", @@ -13967,6 +14023,8 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "inBundle": true, "license": "MIT" @@ -13990,6 +14048,8 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14005,6 +14065,8 @@ }, "node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", + "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", "dev": true, "inBundle": true, "license": "ISC" @@ -14173,6 +14235,8 @@ }, "node_modules/npm/node_modules/@npmcli/name-from-folder": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz", + "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==", "dev": true, "inBundle": true, "license": "ISC", @@ -14182,6 +14246,8 @@ }, "node_modules/npm/node_modules/@npmcli/node-gyp": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", "dev": true, "inBundle": true, "license": "ISC", @@ -14221,6 +14287,8 @@ }, "node_modules/npm/node_modules/@npmcli/query": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/query/-/query-3.1.0.tgz", + "integrity": "sha512-C/iR0tk7KSKGldibYIB9x8GtO/0Bd0I2mhOaDb8ucQL/bQVTmGoeREaFj64Z5+iCBRf3dQfed0CjJL7I8iTkiQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -14259,6 +14327,8 @@ }, "node_modules/npm/node_modules/@pkgjs/parseargs": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14343,6 +14413,8 @@ }, "node_modules/npm/node_modules/@tufjs/canonical-json": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "dev": true, "inBundle": true, "license": "MIT", @@ -14365,6 +14437,8 @@ }, "node_modules/npm/node_modules/abbrev": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -14374,6 +14448,8 @@ }, "node_modules/npm/node_modules/agent-base": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "inBundle": true, "license": "MIT", @@ -14386,6 +14462,8 @@ }, "node_modules/npm/node_modules/aggregate-error": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "inBundle": true, "license": "MIT", @@ -14399,6 +14477,8 @@ }, "node_modules/npm/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14408,6 +14488,8 @@ }, "node_modules/npm/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "inBundle": true, "license": "MIT", @@ -14420,18 +14502,24 @@ }, "node_modules/npm/node_modules/aproba": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/archy": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "inBundle": true, "license": "MIT" @@ -14465,6 +14553,8 @@ }, "node_modules/npm/node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "inBundle": true, "license": "MIT", @@ -14497,6 +14587,8 @@ }, "node_modules/npm/node_modules/chalk": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "inBundle": true, "license": "MIT", @@ -14509,6 +14601,8 @@ }, "node_modules/npm/node_modules/chownr": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -14518,6 +14612,8 @@ }, "node_modules/npm/node_modules/ci-info": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", "dev": true, "funding": [ { @@ -14545,6 +14641,8 @@ }, "node_modules/npm/node_modules/clean-stack": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, "inBundle": true, "license": "MIT", @@ -14554,6 +14652,8 @@ }, "node_modules/npm/node_modules/cli-columns": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-columns/-/cli-columns-4.0.0.tgz", + "integrity": "sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14576,6 +14676,8 @@ }, "node_modules/npm/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14588,18 +14690,24 @@ }, "node_modules/npm/node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/common-ancestor-path": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/cross-spawn": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "inBundle": true, "license": "MIT", @@ -14629,6 +14737,8 @@ }, "node_modules/npm/node_modules/cssesc": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14641,6 +14751,8 @@ }, "node_modules/npm/node_modules/debug": { "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14664,6 +14776,8 @@ }, "node_modules/npm/node_modules/diff": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "inBundle": true, "license": "BSD-3-Clause", @@ -14673,18 +14787,24 @@ }, "node_modules/npm/node_modules/eastasianwidth": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/encoding": { "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, "inBundle": true, "license": "MIT", @@ -14695,6 +14815,8 @@ }, "node_modules/npm/node_modules/env-paths": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "inBundle": true, "license": "MIT", @@ -14704,18 +14826,24 @@ }, "node_modules/npm/node_modules/err-code": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/exponential-backoff": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", "dev": true, "inBundle": true, "license": "Apache-2.0" }, "node_modules/npm/node_modules/fastest-levenshtein": { "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14725,6 +14853,8 @@ }, "node_modules/npm/node_modules/foreground-child": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, "inBundle": true, "license": "ISC", @@ -14741,6 +14871,8 @@ }, "node_modules/npm/node_modules/fs-minipass": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, "inBundle": true, "license": "ISC", @@ -14753,6 +14885,8 @@ }, "node_modules/npm/node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "inBundle": true, "license": "MIT", @@ -14784,12 +14918,16 @@ }, "node_modules/npm/node_modules/graceful-fs": { "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14814,12 +14952,16 @@ }, "node_modules/npm/node_modules/http-cache-semantics": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true, "inBundle": true, "license": "BSD-2-Clause" }, "node_modules/npm/node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "inBundle": true, "license": "MIT", @@ -14833,6 +14975,8 @@ }, "node_modules/npm/node_modules/https-proxy-agent": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14846,6 +14990,8 @@ }, "node_modules/npm/node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "inBundle": true, "license": "MIT", @@ -14871,6 +15017,8 @@ }, "node_modules/npm/node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "inBundle": true, "license": "MIT", @@ -14880,6 +15028,8 @@ }, "node_modules/npm/node_modules/indent-string": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14916,6 +15066,8 @@ }, "node_modules/npm/node_modules/ip-address": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, "inBundle": true, "license": "MIT", @@ -14929,6 +15081,8 @@ }, "node_modules/npm/node_modules/ip-regex": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", "dev": true, "inBundle": true, "license": "MIT", @@ -14953,6 +15107,8 @@ }, "node_modules/npm/node_modules/is-core-module": { "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "inBundle": true, "license": "MIT", @@ -14965,6 +15121,8 @@ }, "node_modules/npm/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14974,12 +15132,16 @@ }, "node_modules/npm/node_modules/is-lambda": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "inBundle": true, "license": "ISC" @@ -15004,6 +15166,8 @@ }, "node_modules/npm/node_modules/jsbn": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "dev": true, "inBundle": true, "license": "MIT" @@ -15019,6 +15183,8 @@ }, "node_modules/npm/node_modules/json-stringify-nice": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", + "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15028,6 +15194,8 @@ }, "node_modules/npm/node_modules/jsonparse": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "dev": true, "engines": [ "node >= 0.2.0" @@ -15037,12 +15205,16 @@ }, "node_modules/npm/node_modules/just-diff": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", + "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/just-diff-apply": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", + "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", "dev": true, "inBundle": true, "license": "MIT" @@ -15271,6 +15443,8 @@ }, "node_modules/npm/node_modules/minipass-collect": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15300,6 +15474,8 @@ }, "node_modules/npm/node_modules/minipass-flush": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15324,6 +15500,8 @@ }, "node_modules/npm/node_modules/minipass-json-stream": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", "dev": true, "inBundle": true, "license": "MIT", @@ -15346,6 +15524,8 @@ }, "node_modules/npm/node_modules/minipass-pipeline": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, "inBundle": true, "license": "ISC", @@ -15370,6 +15550,8 @@ }, "node_modules/npm/node_modules/minipass-sized": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "dev": true, "inBundle": true, "license": "ISC", @@ -15394,6 +15576,8 @@ }, "node_modules/npm/node_modules/minizlib": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "inBundle": true, "license": "MIT", @@ -15419,6 +15603,8 @@ }, "node_modules/npm/node_modules/mkdirp": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, "inBundle": true, "license": "MIT", @@ -15431,6 +15617,8 @@ }, "node_modules/npm/node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "inBundle": true, "license": "MIT" @@ -15446,6 +15634,8 @@ }, "node_modules/npm/node_modules/negotiator": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, "inBundle": true, "license": "MIT", @@ -15518,6 +15708,8 @@ }, "node_modules/npm/node_modules/npm-audit-report": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-audit-report/-/npm-audit-report-5.0.0.tgz", + "integrity": "sha512-EkXrzat7zERmUhHaoren1YhTxFwsOu5jypE84k6632SXTHcQE1z8V51GC6GVZt8LxkC+tbBcKMUBZAgk8SUSbw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15539,6 +15731,8 @@ }, "node_modules/npm/node_modules/npm-install-checks": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", "dev": true, "inBundle": true, "license": "BSD-2-Clause", @@ -15551,6 +15745,8 @@ }, "node_modules/npm/node_modules/npm-normalize-package-bin": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -15575,6 +15771,8 @@ }, "node_modules/npm/node_modules/npm-packlist": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", "dev": true, "inBundle": true, "license": "ISC", @@ -15643,6 +15841,8 @@ }, "node_modules/npm/node_modules/p-map": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -15689,6 +15889,8 @@ }, "node_modules/npm/node_modules/parse-conflict-json": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-3.0.1.tgz", + "integrity": "sha512-01TvEktc68vwbJOtWZluyWeVGWjP+bZwXtPDMQVbBKzbJ/vZBif0L69KH1+cHv1SZ6e0FKLvjyHe8mqsIqYOmw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15703,6 +15905,8 @@ }, "node_modules/npm/node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "inBundle": true, "license": "MIT", @@ -15759,6 +15963,8 @@ }, "node_modules/npm/node_modules/promise-all-reject-late": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", + "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15768,6 +15974,8 @@ }, "node_modules/npm/node_modules/promise-call-limit": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.1.tgz", + "integrity": "sha512-utl+0x8gIDasV5X+PI5qWEPqH6fJS0pFtQ/4gZ95xfEFb/89dmh+/b895TbFDBLiafBvxD/PGTKfvxl4kH/pQg==", "dev": true, "inBundle": true, "license": "ISC", @@ -15777,12 +15985,16 @@ }, "node_modules/npm/node_modules/promise-inflight": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/promise-retry": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, "inBundle": true, "license": "MIT", @@ -15808,6 +16020,8 @@ }, "node_modules/npm/node_modules/qrcode-terminal": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", "dev": true, "inBundle": true, "bin": { @@ -15828,6 +16042,8 @@ }, "node_modules/npm/node_modules/read-cmd-shim": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", + "integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==", "dev": true, "inBundle": true, "license": "ISC", @@ -15837,6 +16053,8 @@ }, "node_modules/npm/node_modules/read-package-json-fast": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15850,6 +16068,8 @@ }, "node_modules/npm/node_modules/retry": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "inBundle": true, "license": "MIT", @@ -15859,6 +16079,8 @@ }, "node_modules/npm/node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "inBundle": true, "license": "MIT", @@ -15878,6 +16100,8 @@ }, "node_modules/npm/node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "inBundle": true, "license": "MIT", @@ -15890,6 +16114,8 @@ }, "node_modules/npm/node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "inBundle": true, "license": "MIT", @@ -15899,6 +16125,8 @@ }, "node_modules/npm/node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15928,6 +16156,8 @@ }, "node_modules/npm/node_modules/smart-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, "inBundle": true, "license": "MIT", @@ -15966,6 +16196,8 @@ }, "node_modules/npm/node_modules/spdx-correct": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -15986,6 +16218,8 @@ }, "node_modules/npm/node_modules/spdx-exceptions": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "inBundle": true, "license": "CC-BY-3.0" @@ -16026,6 +16260,8 @@ }, "node_modules/npm/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "inBundle": true, "license": "MIT", @@ -16041,6 +16277,8 @@ "node_modules/npm/node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "inBundle": true, "license": "MIT", @@ -16055,6 +16293,8 @@ }, "node_modules/npm/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "inBundle": true, "license": "MIT", @@ -16068,6 +16308,8 @@ "node_modules/npm/node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "inBundle": true, "license": "MIT", @@ -16080,6 +16322,8 @@ }, "node_modules/npm/node_modules/supports-color": { "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", "dev": true, "inBundle": true, "license": "MIT", @@ -16142,18 +16386,24 @@ }, "node_modules/npm/node_modules/text-table": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/tiny-relative-date": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz", + "integrity": "sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/treeverse": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz", + "integrity": "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -16177,6 +16427,8 @@ }, "node_modules/npm/node_modules/unique-filename": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", "dev": true, "inBundle": true, "license": "ISC", @@ -16189,6 +16441,8 @@ }, "node_modules/npm/node_modules/unique-slug": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -16201,12 +16455,16 @@ }, "node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/validate-npm-package-license": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -16236,12 +16494,16 @@ }, "node_modules/npm/node_modules/walk-up-path": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", + "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/which": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, "inBundle": true, "license": "ISC", @@ -16266,6 +16528,8 @@ }, "node_modules/npm/node_modules/wrap-ansi": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -16284,6 +16548,8 @@ "node_modules/npm/node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "inBundle": true, "license": "MIT", @@ -16316,6 +16582,8 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, "inBundle": true, "license": "MIT", @@ -16328,6 +16596,8 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "inBundle": true, "license": "MIT" @@ -16351,6 +16621,8 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -16366,6 +16638,8 @@ }, "node_modules/npm/node_modules/write-file-atomic": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "inBundle": true, "license": "ISC", @@ -16379,6 +16653,8 @@ }, "node_modules/npm/node_modules/yallist": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "inBundle": true, "license": "ISC" @@ -17799,6 +18075,14 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -19211,7 +19495,6 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, "license": "0BSD" }, "node_modules/tsup": { diff --git a/package.json b/package.json index be8d24529..9b92739cd 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "typescript-coverage-report": "npm:@penovicp/typescript-coverage-report@^1.0.0-beta.2" }, "dependencies": { + "@ledgerhq/hw-transport": "^6.31.1", "@noble/curves": "~1.4.0", "@noble/hashes": "^1.4.0", "@scure/base": "~1.1.3", diff --git a/src/channel/rpc_0_6.ts b/src/channel/rpc_0_6.ts index 44be230d7..1558f3b85 100644 --- a/src/channel/rpc_0_6.ts +++ b/src/channel/rpc_0_6.ts @@ -18,6 +18,7 @@ import { waitForTransactionOptions, } from '../types'; import { JRPC, RPCSPEC06 as RPC } from '../types/api'; +import { BatchClient } from '../utils/batch'; import { CallData } from '../utils/calldata'; import { isSierra } from '../utils/contract'; import { validateAndParseEthAddress } from '../utils/eth'; @@ -52,8 +53,10 @@ export class RpcChannel { readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed + private batchClient?: BatchClient; + constructor(optionsOrProvider?: RpcProviderOptions) { - const { nodeUrl, retries, headers, blockIdentifier, chainId, specVersion, waitMode } = + const { nodeUrl, retries, headers, blockIdentifier, chainId, specVersion, waitMode, batch } = optionsOrProvider || {}; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default); @@ -69,6 +72,14 @@ export class RpcChannel { this.specVersion = specVersion; this.waitMode = waitMode || false; this.requestId = 0; + + if (typeof batch === 'number') { + this.batchClient = new BatchClient({ + nodeUrl: this.nodeUrl, + headers: this.headers, + interval: batch, + }); + } } public setChainId(chainId: StarknetChainId) { @@ -110,6 +121,16 @@ export class RpcChannel { params?: RPC.Methods[T]['params'] ): Promise { try { + if (this.batchClient) { + const { error, result } = await this.batchClient.fetch( + method, + params, + (this.requestId += 1) + ); + this.errorHandler(method, params, error); + return result as RPC.Methods[T]['result']; + } + const rawResult = await this.fetch(method, params, (this.requestId += 1)); const { error, result } = await rawResult.json(); this.errorHandler(method, params, error); diff --git a/src/channel/rpc_0_7.ts b/src/channel/rpc_0_7.ts index 6d0f9ba9a..cd14a8b77 100644 --- a/src/channel/rpc_0_7.ts +++ b/src/channel/rpc_0_7.ts @@ -18,6 +18,7 @@ import { waitForTransactionOptions, } from '../types'; import { JRPC, RPCSPEC07 as RPC } from '../types/api'; +import { BatchClient } from '../utils/batch'; import { CallData } from '../utils/calldata'; import { isSierra } from '../utils/contract'; import { validateAndParseEthAddress } from '../utils/eth'; @@ -54,6 +55,8 @@ export class RpcChannel { readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed + private batchClient?: BatchClient; + constructor(optionsOrProvider?: RpcProviderOptions) { const { nodeUrl, @@ -64,6 +67,7 @@ export class RpcChannel { specVersion, waitMode, transactionRetryIntervalFallback, + batch, } = optionsOrProvider || {}; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default); @@ -80,6 +84,14 @@ export class RpcChannel { this.waitMode = waitMode || false; this.requestId = 0; this.transactionRetryIntervalFallback = transactionRetryIntervalFallback; + + if (typeof batch === 'number') { + this.batchClient = new BatchClient({ + nodeUrl: this.nodeUrl, + headers: this.headers, + interval: batch, + }); + } } private get transactionRetryIntervalDefault() { @@ -125,6 +137,16 @@ export class RpcChannel { params?: RPC.Methods[T]['params'] ): Promise { try { + if (this.batchClient) { + const { error, result } = await this.batchClient.fetch( + method, + params, + (this.requestId += 1) + ); + this.errorHandler(method, params, error); + return result as RPC.Methods[T]['result']; + } + const rawResult = await this.fetch(method, params, (this.requestId += 1)); const { error, result } = await rawResult.json(); this.errorHandler(method, params, error); diff --git a/src/constants.ts b/src/constants.ts index 228bef719..ba87c28ff 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,7 @@ export { ETransactionVersion as TRANSACTION_VERSION }; export const ZERO = 0n; export const MASK_250 = 2n ** 250n - 1n; // 2 ** 250 - 1 +export const MASK_31 = 2n ** 31n - 1n; // 2 ** 31 - 1 export const API_VERSION = ZERO; export const PRIME = 2n ** 251n + 17n * 2n ** 192n + 1n; diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index 65d8c0ac4..548e1215f 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -1,7 +1,4 @@ -import { bytesToHex } from '@noble/curves/abstract/utils'; -import { keccak_256 } from '@noble/hashes/sha3'; import type { SPEC } from 'starknet-types-07'; - import { RPC06, RPC07, RpcChannel } from '../channel'; import { AccountInvocations, @@ -34,13 +31,13 @@ import type { TransactionWithHash } from '../types/provider/spec'; import assert from '../utils/assert'; import { getAbiContractVersion } from '../utils/calldata/cairo'; import { isSierra } from '../utils/contract'; -import { addHexPrefix, removeHexPrefix } from '../utils/encode'; -import { hexToBytes, toHex } from '../utils/num'; +import { toHex } from '../utils/num'; import { wait } from '../utils/provider'; import { RPCResponseParser } from '../utils/responseParser/rpc'; import { GetTransactionReceiptResponse, ReceiptTx } from '../utils/transactionReceipt'; import { LibraryError } from './errors'; import { ProviderInterface } from './interface'; +import { solidityUint256PackedKeccak256 } from '../utils/hash'; export class RpcProvider implements ProviderInterface { public responseParser: RPCResponseParser; @@ -115,7 +112,7 @@ export class RpcProvider implements ProviderInterface { /** * Pause the execution of the script until a specified block is created. - * @param {BlockIdentifier} blockIdentifier bloc number (BigNumberisk) or 'pending' or 'latest'. + * @param {BlockIdentifier} blockIdentifier bloc number (BigNumberish) or 'pending' or 'latest'. * Use of 'latest" or of a block already created will generate no pause. * @param {number} [retryInterval] number of milliseconds between 2 requests to the node * @example @@ -160,7 +157,7 @@ export class RpcProvider implements ProviderInterface { .then(this.responseParser.parseL1GasPriceResponse); } - public async getL1MessageHash(l2TxHash: BigNumberish) { + public async getL1MessageHash(l2TxHash: BigNumberish): Promise { const transaction = (await this.channel.getTransactionByHash(l2TxHash)) as TransactionWithHash; assert(transaction.type === 'L1_HANDLER', 'This L2 transaction is not a L1 message.'); const { calldata, contract_address, entry_point_selector, nonce } = @@ -173,13 +170,7 @@ export class RpcProvider implements ProviderInterface { calldata.length - 1, ...calldata.slice(1), ]; - const myEncode = addHexPrefix( - params.reduce( - (res: string, par: BigNumberish) => res + removeHexPrefix(toHex(par)).padStart(64, '0'), - '' - ) - ); - return addHexPrefix(bytesToHex(keccak_256(hexToBytes(myEncode)))); + return solidityUint256PackedKeccak256(params); } public async getBlockWithReceipts(blockIdentifier?: BlockIdentifier) { diff --git a/src/signer/index.ts b/src/signer/index.ts index 1ec304d6c..ff56b157a 100644 --- a/src/signer/index.ts +++ b/src/signer/index.ts @@ -1,3 +1,4 @@ export * from './interface'; export * from './default'; export * from './ethSigner'; +export * from './ledgerSigner'; diff --git a/src/signer/ledgerSigner.ts b/src/signer/ledgerSigner.ts new file mode 100644 index 000000000..c6471e5a1 --- /dev/null +++ b/src/signer/ledgerSigner.ts @@ -0,0 +1,267 @@ +import Transport from '@ledgerhq/hw-transport'; +import type { + InvocationsSignerDetails, + V2InvocationsSignerDetails, + V3InvocationsSignerDetails, + DeployAccountSignerDetails, + V2DeployAccountSignerDetails, + V3DeployAccountSignerDetails, + DeclareSignerDetails, + V2DeclareSignerDetails, + V3DeclareSignerDetails, + TypedData, + Call, + Signature, +} from '../types'; +import assert from '../utils/assert'; +import { CallData } from '../utils/calldata'; +import type { SignerInterface } from './interface'; +import { MASK_31 } from '../constants'; +import { ETransactionVersion2 } from '../types/api/rpcspec_0_6'; +import { getMessageHash } from '../utils/typedData'; +import { getExecuteCalldata } from '../utils/transaction'; +import { + calculateDeclareTransactionHash, + calculateDeployAccountTransactionHash, + calculateInvokeTransactionHash, +} from '../utils/hash'; +import { intDAM } from '../utils/stark'; +import { addHexPrefix, buf2hex, concatenateArrayBuffer, removeHexPrefix } from '../utils/encode'; +import { hexToBytes, stringToSha256ToArrayBuff4, toHex } from '../utils/num'; +import { starkCurve } from '../utils/ec'; +import { ETransactionVersion3 } from '../types/api'; + +/** + * Signer for accounts using a Ledger Nano S+/X signature + */ +export class LedgerSigner implements SignerInterface { + readonly transporter: Transport; + + readonly accountID: number; + + readonly eip2645applicationName: string; + + readonly pathBuffer: Uint8Array; + + private appVersion: string; + + protected pubKey: string; + + protected fullPubKey: string; + + /** + * constructor of the LedgerSigner class. + * @param {Transport} transport 5 transports are available to handle USB, bluetooth, Node, Web, Mobile. + * See Guides for more details. + * @param {number} accountID ID of Ledger Nano (can handle 2**31 accounts). + * @param {string} [eip2645application='LedgerW'] A wallet is defined by an ERC2645 derivation path (6 items). + * One item is the `application`. Default value is `LedgerW`. + * @example + * ```typescript + * import TransportNodeHid from "@ledgerhq/hw-transport-node-hid"; + * const myNodeTransport = await TransportNodeHid.create(); + * const myLedgerSigner = new LedgerSigner(myNodeTransport, 0); + * ``` + */ + constructor(transport: Transport, accountID: number, eip2645application: string = 'LedgerW') { + assert(accountID >= 0, 'Ledger account ID shall not be a negative number.'); + assert(accountID <= MASK_31, 'Ledger account ID shall be < 2**31.'); + assert(!!eip2645application, 'Ledger application name shall not be empty.'); + this.transporter = transport; + this.accountID = accountID; + this.pubKey = ''; + this.fullPubKey = ''; + this.eip2645applicationName = eip2645application; + this.appVersion = ''; + this.pathBuffer = getLedgerPathBuffer(this.accountID, this.eip2645applicationName); + } + + /** + * provides the Starknet public key + * @returns an hex string : 64 characters are Point X coordinate. + */ + public async getPubKey(): Promise { + if (!this.pubKey) await this.getPublicKeys(); + return this.pubKey; + } + + /** + * provides the full public key (with parity prefix) + * @returns an hex string : 2 first characters are the parity, the 64 following characters are Point X coordinate. 64 last characters are Point Y coordinate. + */ + public async getFullPubKey(): Promise { + if (!this.fullPubKey) await this.getPublicKeys(); + return this.fullPubKey; + } + + /** + * Returns the version of the Starknet APP implemented in the Ledger. + * @returns {string} version. + * @example + * ```typescript + * const result = await myLedgerSigner.getAppVersion(); + * // result= "1.1.1" + * ``` + */ + public async getAppVersion(): Promise { + if (!this.appVersion) { + const resp = await this.transporter.send(Number('0x5a'), 0, 0, 0); + this.appVersion = `${resp[0]}.${resp[1]}.${resp[2]}`; + } + return this.appVersion; + } + + public async signMessage(typedDataToHash: TypedData, accountAddress: string): Promise { + const msgHash = getMessageHash(typedDataToHash, accountAddress); + return this.signRaw(msgHash); + } + + public async signTransaction( + transactions: Call[], + transactionsDetail: InvocationsSignerDetails + ): Promise { + const compiledCalldata = getExecuteCalldata(transactions, transactionsDetail.cairoVersion); + let msgHash; + + // TODO: How to do generic union discriminator for all like this + if (Object.values(ETransactionVersion2).includes(transactionsDetail.version as any)) { + const det = transactionsDetail as V2InvocationsSignerDetails; + msgHash = calculateInvokeTransactionHash({ + ...det, + senderAddress: det.walletAddress, + compiledCalldata, + version: det.version, + }); + } else if (Object.values(ETransactionVersion3).includes(transactionsDetail.version as any)) { + const det = transactionsDetail as V3InvocationsSignerDetails; + msgHash = calculateInvokeTransactionHash({ + ...det, + senderAddress: det.walletAddress, + compiledCalldata, + version: det.version, + nonceDataAvailabilityMode: intDAM(det.nonceDataAvailabilityMode), + feeDataAvailabilityMode: intDAM(det.feeDataAvailabilityMode), + }); + } else { + throw Error('unsupported signTransaction version'); + } + + return this.signRaw(msgHash as string); + } + + public async signDeployAccountTransaction( + details: DeployAccountSignerDetails + ): Promise { + const compiledConstructorCalldata = CallData.compile(details.constructorCalldata); + /* const version = BigInt(details.version).toString(); */ + let msgHash; + + if (Object.values(ETransactionVersion2).includes(details.version as any)) { + const det = details as V2DeployAccountSignerDetails; + msgHash = calculateDeployAccountTransactionHash({ + ...det, + salt: det.addressSalt, + constructorCalldata: compiledConstructorCalldata, + version: det.version, + }); + } else if (Object.values(ETransactionVersion3).includes(details.version as any)) { + const det = details as V3DeployAccountSignerDetails; + msgHash = calculateDeployAccountTransactionHash({ + ...det, + salt: det.addressSalt, + compiledConstructorCalldata, + version: det.version, + nonceDataAvailabilityMode: intDAM(det.nonceDataAvailabilityMode), + feeDataAvailabilityMode: intDAM(det.feeDataAvailabilityMode), + }); + } else { + throw Error('unsupported signDeployAccountTransaction version'); + } + + return this.signRaw(msgHash as string); + } + + public async signDeclareTransaction( + // contractClass: ContractClass, // Should be used once class hash is present in ContractClass + details: DeclareSignerDetails + ): Promise { + let msgHash; + + if (Object.values(ETransactionVersion2).includes(details.version as any)) { + const det = details as V2DeclareSignerDetails; + msgHash = calculateDeclareTransactionHash({ + ...det, + version: det.version, + }); + } else if (Object.values(ETransactionVersion3).includes(details.version as any)) { + const det = details as V3DeclareSignerDetails; + msgHash = calculateDeclareTransactionHash({ + ...det, + version: det.version, + nonceDataAvailabilityMode: intDAM(det.nonceDataAvailabilityMode), + feeDataAvailabilityMode: intDAM(det.feeDataAvailabilityMode), + }); + } else { + throw Error('unsupported signDeclareTransaction version'); + } + + return this.signRaw(msgHash as string); + } + + private async signRaw(msgHash: string): Promise { + addHexPrefix( + buf2hex(await this.transporter.send(Number('0x5a'), 2, 0, 0, Buffer.from(this.pathBuffer))) + ); + // eslint-disable-next-line no-bitwise + const shiftedHash = toHex(BigInt(msgHash) << 4n); + const buff2 = hexToBytes(shiftedHash); + const respSign2 = Uint8Array.from( + await this.transporter.send(Number('0x5a'), 2, 1, 0, Buffer.from(buff2)) + ); + const r = BigInt(addHexPrefix(buf2hex(respSign2.subarray(1, 33)))); + const s = BigInt(addHexPrefix(buf2hex(respSign2.subarray(33, 65)))); + const v = respSign2[65]; + const sign0 = new starkCurve.Signature(r, s); + const sign1 = sign0.addRecoveryBit(v); + return sign1; + } + + private async getPublicKeys() { + const pathBuff = this.pathBuffer; + const respGetPublic = Uint8Array.from( + await this.transporter.send(Number('0x5a'), 1, 0, 0, Buffer.from(pathBuff)) + ); + this.pubKey = addHexPrefix(buf2hex(respGetPublic.subarray(1, 33))); + this.fullPubKey = addHexPrefix(buf2hex(respGetPublic.subarray(0, 65))); + } +} + +/** + * format the Ledger wallet path to an Uint8Array. + * EIP2645 path = 2645'/starknet'/application'/0'/accountId'/0 + * @param {number} accountId Id of account. < 2**31. + * @param {string} [applicationName='LedgerW'] utf8 string of application name. + * @returns an Uint8array of 24 bytes. + */ +export function getLedgerPathBuffer(accountId: number, applicationName: string): Uint8Array { + const path0buff = new Uint8Array([128, 0, 10, 85]); // "0x80000A55" EIP2645; + const path1buff = new Uint8Array([71, 65, 233, 201]); // "starknet" + const path2buff = + applicationName === 'LedgerW' + ? new Uint8Array([43, 206, 231, 219]) + : stringToSha256ToArrayBuff4(applicationName); + const path3buff = new Uint8Array([0, 0, 0, 0]); + const hex = toHex(accountId); + const padded = addHexPrefix(removeHexPrefix(hex).padStart(8, '0')); + const path4buff = hexToBytes(padded); + const path5buff = new Uint8Array([0, 0, 0, 0]); + const pathBuff = concatenateArrayBuffer([ + path0buff, + path1buff, + path2buff, + path3buff, + path4buff, + path5buff, + ]); + return pathBuff; +} diff --git a/src/types/provider/configuration.ts b/src/types/provider/configuration.ts index db1825f42..7076ea2c8 100644 --- a/src/types/provider/configuration.ts +++ b/src/types/provider/configuration.ts @@ -18,4 +18,5 @@ export type RpcProviderOptions = { l1BoundMaxPricePerUnit: number; maxFee: number; }; + batch?: false | number; }; diff --git a/src/utils/batch/index.ts b/src/utils/batch/index.ts new file mode 100644 index 000000000..f8130c83a --- /dev/null +++ b/src/utils/batch/index.ts @@ -0,0 +1,129 @@ +import { stringify } from '../json'; +import { RPC } from '../../types'; +import { JRPC } from '../../types/api'; + +export type BatchClientOptions = { + nodeUrl: string; + headers: object; + interval: number; +}; + +export class BatchClient { + public nodeUrl: string; + + public headers: object; + + public interval: number; + + public requestId: number = 0; + + private pendingRequests: Record = {}; + + private batchPromises: Record> = {}; + + private delayTimer?: NodeJS.Timeout; + + private delayPromise?: Promise; + + private delayPromiseResolve?: () => void; + + constructor(options: BatchClientOptions) { + this.nodeUrl = options.nodeUrl; + this.headers = options.headers; + this.interval = options.interval; + } + + private async wait(): Promise { + // If the promise is not set, create a new one and store the resolve function + if (!this.delayPromise || !this.delayPromiseResolve) { + this.delayPromise = new Promise((resolve) => { + this.delayPromiseResolve = resolve; + }); + } + + if (this.delayTimer) { + clearTimeout(this.delayTimer); + this.delayTimer = undefined; + } + + this.delayTimer = setTimeout(() => { + if (this.delayPromiseResolve) { + this.delayPromiseResolve(); + + // Reset the promise and resolve function so that a new promise is created next time + this.delayPromise = undefined; + this.delayPromiseResolve = undefined; + } + }, this.interval); + + return this.delayPromise; + } + + private addPendingRequest( + method: T, + params?: RPC.Methods[T]['params'], + id?: string | number + ) { + const request: JRPC.RequestBody = { + id: id ?? `batched_${(this.requestId += 1)}`, + jsonrpc: '2.0', + method, + params: params ?? undefined, + }; + + this.pendingRequests[request.id] = request; + + return request.id; + } + + private async sendBatch(requests: JRPC.RequestBody[]) { + const raw = await fetch(this.nodeUrl, { + method: 'POST', + body: stringify(requests), + headers: this.headers as Record, + }); + + return raw.json(); + } + + /** + * Automatically batches and fetches JSON-RPC calls in a single request. + * @param method Method to call + * @param params Method parameters + * @param id JSON-RPC Request ID + * @returns JSON-RPC Response + */ + public async fetch< + T extends keyof RPC.Methods, + TResponse extends JRPC.ResponseBody & { + result?: RPC.Methods[T]['result']; + error?: JRPC.Error; + }, + >(method: T, params?: RPC.Methods[T]['params'], id?: string | number): Promise { + const requestId = this.addPendingRequest(method, params, id); + + // Wait for the interval to pass before sending the batch + await this.wait(); + + // Get the pending requests and clear the object + const requests = this.pendingRequests; + this.pendingRequests = {}; + + // If there is no promise for this batch, create one and send the batch + if (!this.batchPromises[requestId]) { + const promise = this.sendBatch(Object.values(requests)); + Object.keys(requests).forEach((key) => { + this.batchPromises[key] = promise; + }); + } + + const results = await this.batchPromises[requestId]; + delete this.batchPromises[requestId]; + + // Find this request in the results and return it + const result = results.find((res: any) => res.id === requestId); + if (!result) throw new Error(`Couldn't find the result for the request. Method: ${method}`); + + return result as TResponse; + } +} diff --git a/src/utils/encode.ts b/src/utils/encode.ts index 4095e6ba6..6b9878a40 100644 --- a/src/utils/encode.ts +++ b/src/utils/encode.ts @@ -292,3 +292,27 @@ export const pascalToSnake = (text: string) => .join('_') .toUpperCase() : text; + +/** + * Combine multiple Uint8Arrays into one. + * Useful for wallet path creation. + * @param {Uint8Array[]} uint8arrays An array of Uint8Array. + * @returns {Uint8Array} all the Uint8Arrays joined. + * @example + * ```typescript + * const path0buff = new Uint8Array([128, 0, 10, 85]); + * const path1buff = new Uint8Array([71, 65, 233, 201]); + * const result = encode.concatenateArrayBuffer([path0buff, path1buff]); + * // result = Uint8Array(8) [128, 0, 10, 85, 71, 65, 233, 201] + * ``` + */ +export function concatenateArrayBuffer(uint8arrays: Uint8Array[]): Uint8Array { + const totalLength = uint8arrays.reduce((total, uint8array) => total + uint8array.byteLength, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + uint8arrays.forEach((uint8array) => { + result.set(uint8array, offset); + offset += uint8array.byteLength; + }); + return result; +} diff --git a/src/utils/hash/selector.ts b/src/utils/hash/selector.ts index 063b662a8..87d4dd918 100644 --- a/src/utils/hash/selector.ts +++ b/src/utils/hash/selector.ts @@ -1,12 +1,13 @@ import { keccak } from '@scure/starknet'; - +import { keccak_256 } from '@noble/hashes/sha3'; +import { bytesToHex } from '@noble/curves/abstract/utils'; import { MASK_250 } from '../../constants'; import { BigNumberish } from '../../types'; import { addHexPrefix, removeHexPrefix, utf8ToArray } from '../encode'; -import { hexToBytes, isHex, isStringWholeNumber, toHex, toHexString } from '../num'; +import { hexToBytes, isBigInt, isHex, isNumber, isStringWholeNumber, toHex } from '../num'; /** - * Calculate the hex-string Keccak hash for a given BigNumberish + * Calculate the hex-string Starknet Keccak hash for a given BigNumberish * * @param value value to hash * @returns hex-string Keccak hash @@ -24,7 +25,7 @@ export function keccakBn(value: BigNumberish): string { /** * [internal] - * Calculate hex-string Keccak hash for a given string + * Calculate hex-string Starknet Keccak hash for a given string * * String -> hex-string Keccak hash * @returns format: hex-string @@ -69,9 +70,9 @@ export function getSelectorFromName(funcName: string) { } /** - * Calculate the hex-string selector from a given abi function name, decimal string or hex string + * Calculate the hex-string selector from a given abi function name or of any representation of number. * - * @param value hex-string | dec-string | ascii-string + * @param value ascii-string | hex-string | dec-string | number | BigInt * @returns hex-string selector * @example * ```typescript @@ -83,14 +84,76 @@ export function getSelectorFromName(funcName: string) { * * const selector3: string = getSelector("123456"); * // selector3 = "0x1e240" + * + * const selector4: string = getSelector(123456n); + * // selector4 = "0x1e240" * ``` */ -export function getSelector(value: string) { - if (isHex(value)) { - return value; - } - if (isStringWholeNumber(value)) { - return toHexString(value); - } +export function getSelector(value: string | BigNumberish) { + if (isNumber(value) || isBigInt(value)) return toHex(value); + if (isHex(value)) return value; + if (isStringWholeNumber(value)) return toHex(value); return getSelectorFromName(value); } + +/** + * Solidity hash of an array of uint256 + * @param {BigNumberish[]} params an array of uint256 numbers + * @returns the hash of the array of Solidity uint256 + * @example + * ```typescript + * const result = hash.solidityUint256PackedKeccak256(['0x100', '200', 300, 400n]); + * // result = '0xd1e6cb422b65269603c491b0c85463295edabebfb2a6844e4fdc389ff1dcdd97' + * ``` + */ +export function solidityUint256PackedKeccak256(params: BigNumberish[]): string { + const myEncode = addHexPrefix( + params.reduce( + (res: string, par: BigNumberish) => res + removeHexPrefix(toHex(par)).padStart(64, '0'), + '' + ) + ); + return addHexPrefix(bytesToHex(keccak_256(hexToBytes(myEncode)))); +} + +/** + * Calculate the L2 message hash related by a message L1->L2 + * @param {BigNumberish} l1FromAddress L1 account address that paid the message. + * @param {BigNumberish} l2ToAddress L2 contract address to execute. + * @param {string | BigNumberish} l2Selector can be a function name ("bridge_withdraw") or a number (BigNumberish). + * @param {RawCalldata} l2Calldata an array of BigNumberish of the raw parameters passed to the above function. + * @param {BigNumberish} l1Nonce The nonce of the L1 account. + * @returns {string} hex-string of the L2 transaction hash + * @example + * ```typescript + * const l1FromAddress = "0x0000000000000000000000008453fc6cd1bcfe8d4dfc069c400b433054d47bdc"; + * const l2ToAddress = 2158142789748719025684046545159279785659305214176670733242887773692203401023n; + * const l2Selector = 774397379524139446221206168840917193112228400237242521560346153613428128537n; + * const payload = [ + * 4543560n, + * 829565602143178078434185452406102222830667255948n, + * 3461886633118033953192540141609307739580461579986333346825796013261542798665n, + * 9000000000000000n, + * 0n, + * ]; + * const l1Nonce = 8288n; + * const result = hash.getL2MessageHash(l1FromAddress, l2ToAddress, l2Selector, payload, l1Nonce); + * // result = "0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090" + * ``` + */ +export function getL2MessageHash( + l1FromAddress: BigNumberish, + l2ToAddress: BigNumberish, + l2Selector: string | BigNumberish, + l2Calldata: BigNumberish[], + l1Nonce: BigNumberish +): string { + return solidityUint256PackedKeccak256([ + l1FromAddress, + l2ToAddress, + l1Nonce, + l2Selector, + l2Calldata.length, + ...l2Calldata, + ]); +} diff --git a/src/utils/hash/transactionHash/index.ts b/src/utils/hash/transactionHash/index.ts index cc0a2e3cf..f0a28d6f4 100644 --- a/src/utils/hash/transactionHash/index.ts +++ b/src/utils/hash/transactionHash/index.ts @@ -22,6 +22,7 @@ import { calculateInvokeTransactionHash as v3calculateInvokeTransactionHash, } from './v3'; +export { calculateL2MessageTxHash } from './v2'; /* * INVOKE TX HASH */ diff --git a/src/utils/hash/transactionHash/v2.ts b/src/utils/hash/transactionHash/v2.ts index 253824a6b..1ba81088c 100644 --- a/src/utils/hash/transactionHash/v2.ts +++ b/src/utils/hash/transactionHash/v2.ts @@ -8,6 +8,7 @@ import { StarknetChainId, TransactionHashPrefix } from '../../../constants'; import { BigNumberish, RawCalldata } from '../../../types'; import { starkCurve } from '../../ec'; import { toBigInt } from '../../num'; +import { getSelector } from '../selector'; /** * Compute pedersen hash from data @@ -127,3 +128,50 @@ export function calculateTransactionHash( [nonce] ); } + +/** + * Calculate the L2 transaction hash generated by a message L1->L2 + * @param {BigNumberish} l1FromAddress L1 account address that paid the message. + * @param {BigNumberish} l2ToAddress L2 contract address to execute. + * @param {string | BigNumberish} l2Selector can be a function name ("bridge_withdraw") or a number (BigNumberish). + * @param {RawCalldata} l2Calldata an array of BigNumberish of the raw parameters passed to the above function. + * @param {BigNumberish} l2ChainId L2 chain ID : from constants.StarknetChainId.xxx + * @param {BigNumberish} l1Nonce The nonce of the L1 account. + * @returns {string} hex-string of the L2 transaction hash + * @example + * ```typescript + * const l1FromAddress = "0x0000000000000000000000008453fc6cd1bcfe8d4dfc069c400b433054d47bdc"; + * const l2ToAddress = 2158142789748719025684046545159279785659305214176670733242887773692203401023n; + * const l2Selector = 774397379524139446221206168840917193112228400237242521560346153613428128537n; + * const payload = [ + * 4543560n, + * 829565602143178078434185452406102222830667255948n, + * 3461886633118033953192540141609307739580461579986333346825796013261542798665n, + * 9000000000000000n, + * 0n, + * ]; + * const l1Nonce = 8288n; + * const result = hash.calculateL2MessageTxHash(l1FromAddress, l2ToAddress, l2Selector, payload, constants.StarknetChainId.SN_SEPOLIA, l1Nonce); + * // result = "0x67d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07" + * ``` + */ +export function calculateL2MessageTxHash( + l1FromAddress: BigNumberish, + l2ToAddress: BigNumberish, + l2Selector: string | BigNumberish, + l2Calldata: RawCalldata, + l2ChainId: StarknetChainId, + l1Nonce: BigNumberish +): string { + const payload = [l1FromAddress, ...l2Calldata]; + return calculateTransactionHashCommon( + TransactionHashPrefix.L1_HANDLER, + 0, + l2ToAddress, + getSelector(l2Selector), + payload, + 0, + l2ChainId, + [l1Nonce] + ); +} diff --git a/src/utils/num.ts b/src/utils/num.ts index c3211f057..5c5e622d2 100644 --- a/src/utils/num.ts +++ b/src/utils/num.ts @@ -1,8 +1,9 @@ import { hexToBytes as hexToBytesNoble } from '@noble/curves/abstract/utils'; - +import { sha256 } from '@noble/hashes/sha256'; import { BigNumberish } from '../types'; import assert from './assert'; -import { addHexPrefix, removeHexPrefix } from './encode'; +import { addHexPrefix, buf2hex, removeHexPrefix } from './encode'; +import { MASK_31 } from '../constants'; /** @deprecated prefer importing from 'types' over 'num' */ export type { BigNumberish }; @@ -376,3 +377,23 @@ export function isNumber(value: unknown): value is number { export function isBoolean(value: unknown): value is boolean { return typeof value === 'boolean'; } + +/** + * Calculate the sha256 hash of an utf8 string, then encode the + * result in an uint8Array of 4 elements. + * Useful in wallet path calculation. + * @param {string} str utf8 string (hex string not handled). + * @returns a uint8Array of 4 bytes. + * @example + * ```typescript + * const ledgerPathApplicationName = 'LedgerW'; + * const path2Buffer = num.stringToSha256ToArrayBuff4(ledgerPathApplicationName); + * // path2Buffer = Uint8Array(4) [43, 206, 231, 219] + * ``` + */ +export function stringToSha256ToArrayBuff4(str: string): Uint8Array { + // eslint-disable-next-line no-bitwise + const int31 = (n: bigint) => Number(n & MASK_31); + const result: number = int31(BigInt(addHexPrefix(buf2hex(sha256(str))))); + return hexToBytes(toHex(result)); +} diff --git a/www/docs/guides/L1message.md b/www/docs/guides/L1message.md index d4f8ecc30..22708d55b 100644 --- a/www/docs/guides/L1message.md +++ b/www/docs/guides/L1message.md @@ -8,7 +8,7 @@ You can exchange messages between L1 & L2 networks: - L2 Starknet mainnet ↔️ L1 Ethereum. - L2 Starknet testnet ↔️ L1 Sepolia ETH testnet. -- L2 local Starknet devnet ↔️ L1 local ETH testnet (Ganache, ...). +- L2 local Starknet devnet ↔️ L1 local ETH testnet (anvil, ...). You can find an explanation of the global mechanism [here](https://docs.starknet.io/documentation/architecture_and_concepts/L1-L2_Communication/messaging-mechanism/). @@ -48,6 +48,52 @@ const responseEstimateMessageFee = await provider.estimateMessageFee({ If the fee is paid in L1, the Cairo contract at `to_Address` is automatically executed, function `entry_point_selector` (the function shall have a decorator `#[l1_handler]` in the Cairo code, with a first parameter called `from_address: felt252`). The payload shall not include the `from_address` parameter. +### L1 ➡️ L2 hashes + +Starknet.js proposes 2 functions to calculate hashes related to a L1 ➡️ L2 message : + +- The L2 message hash: + For a L1 tx requesting a message L1->L2, some data extracted from etherscan : https://sepolia.etherscan.io/tx/0xd82ce7dd9f3964d89d2eb9d555e1460fb7792be274950abe578d610f95cc40f5 + + ```typescript + const l1FromAddress = '0x0000000000000000000000008453fc6cd1bcfe8d4dfc069c400b433054d47bdc'; + const l2ToAddress = 2158142789748719025684046545159279785659305214176670733242887773692203401023n; + const l2Selector = 774397379524139446221206168840917193112228400237242521560346153613428128537n; + const payload = [ + 4543560n, + 829565602143178078434185452406102222830667255948n, + 3461886633118033953192540141609307739580461579986333346825796013261542798665n, + 9000000000000000n, + 0n, + ]; + const l1Nonce = 8288n; + const l1ToL2MessageHash = hash.getL2MessageHash( + l1FromAddress, + l2ToAddress, + l2Selector, + payload, + l1Nonce + ); + // l1ToL2MessageHash = '0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090' + ``` + + Can be verified here : https://sepolia.starkscan.co/message/0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090#messagelogs + +- The L2 transaction hash: + For the same message: + ```typescript + const l1ToL2TransactionHash = hash.calculateL2MessageTxHash( + l1FromAddress, + l2ToAddress, + l2Selector, + payload, + constants.StarknetChainId.SN_SEPOLIA, + l1Nonce + ); + // l1ToL2TransactionHash = '0x67d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07' + ``` + Can be verified here : https://sepolia.starkscan.co/tx/0x067d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07 + ## L2 ➡️ L1 messages To send a message to L1, you will just invoke a Cairo contract function, paying a fee that will pay all the processes (in L1 & L2). @@ -63,3 +109,15 @@ const { suggestedMaxFee: estimatedFee1 } = await account0.estimateInvokeFee({ ``` The result is in `estimatedFee1`, of type BN. + +### L2 ➡️ L1 hash + +Starknet.js proposes a function to calculate the L1 ➡️ L2 message hash : + +```typescript +const l2TransactionHash = '0x28dfc05eb4f261b37ddad451ff22f1d08d4e3c24dc646af0ec69fa20e096819'; +const l1MessageHash = await provider.getL1MessageHash(l2TransactionHash); +// l1MessageHash = '0x55b3f8b6e607fffd9b4d843dfe8f9b5c05822cd94fcad8797deb01d77805532a' +``` + +Can be verified here : https://sepolia.voyager.online/tx/0x28dfc05eb4f261b37ddad451ff22f1d08d4e3c24dc646af0ec69fa20e096819#messages diff --git a/www/docs/guides/connect_network.md b/www/docs/guides/connect_network.md index 9abc87917..5eea392c5 100644 --- a/www/docs/guides/connect_network.md +++ b/www/docs/guides/connect_network.md @@ -160,3 +160,43 @@ const provider = new RpcProvider({ nodeUrl: 'http://127.0.0.1:5050/rpc' }); ``` > If you have customized host and port during starknet-devnet initialization, adapt in accordance your script. + +## Batch JSON-RPC + +The BatchClient class allows requests to be batched together in a single HTTP request, either by the interval amount or at the end of the callback queue if the batch is set to 0. By batching requests, we can reduce the overhead associated with handling individual requests. + +#### Example of usage with RpcProvider + +```typescript +const myProvider = new RpcProvider({ + batch: 0, +}); + +const [getBlockResponse, blockHashAndNumber, txCount] = await Promise.all([ + myBatchProvider.getBlock(), + myBatchProvider.getBlockLatestAccepted(), + myBatchProvider.getBlockTransactionCount('latest'), +]); + +// ... usage of getBlockResponse, blockHashAndNumber, txCount +``` + +#### Example of direct usage of underlying BatchClient class + +```typescript +const provider = new RpcProvider(); + +const batchClient = new BatchClient({ + nodeUrl: provider.channel.nodeUrl, + headers: provider.channel.headers, + interval: 0, +}); + +const [getBlockResponse, blockHashAndNumber, txCount] = await Promise.all([ + myBatchProvider.getBlock(), + myBatchProvider.getBlockLatestAccepted(), + myBatchProvider.getBlockTransactionCount('latest'), +]); + +// ... usage of getBlockResponse, blockHashAndNumber, txCount +``` diff --git a/www/docs/guides/pictures/LedgerConnectivity.png b/www/docs/guides/pictures/LedgerConnectivity.png new file mode 100644 index 000000000..248064f22 Binary files /dev/null and b/www/docs/guides/pictures/LedgerConnectivity.png differ diff --git a/www/docs/guides/pictures/LedgerTitle.png b/www/docs/guides/pictures/LedgerTitle.png new file mode 100644 index 000000000..c39ff5ef7 Binary files /dev/null and b/www/docs/guides/pictures/LedgerTitle.png differ diff --git a/www/docs/guides/signature.md b/www/docs/guides/signature.md index 57d41c938..740497ffa 100644 --- a/www/docs/guides/signature.md +++ b/www/docs/guides/signature.md @@ -191,7 +191,7 @@ try { } ``` -### Signing with an Ethereum signer +## Signing with an Ethereum signer All the previous examples are using the standard Starknet signature process, but you can also use the Ethereum one. @@ -204,3 +204,58 @@ console.log('Complete public key =', await myEthSigner.getPubKey()); const sig0 = await myEthSigner.signMessage(message, myEthAccountAddressInStarknet); console.log('signature message =', sig0); ``` + +## Signing with a Ledger hardware wallet + +![](./pictures/LedgerTitle.png) + +Starknet.js has a support for Ledger Nano S+ or X, to sign your Starknet transactions. +You have to use a transporter to interact with the Ledger Nano. Depending if you use an USB or a Bluetooth connection, depending of your framework (Node, Web, Mobile), you have to use the appropriate library to create your transporter. + +The Ledger documentation is listing all the available cases : +![](./pictures/LedgerConnectivity.png) + +The libs available are : + +```typescript +import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'; +import TransportWebHid from '@ledgerhq/hw-transport-webhid'; +import TransportWebBluetooth from '@ledgerhq/hw-transport-web-ble'; +import TransportHID from '@ledgerhq/react-native-hid'; +import TransportBLE from '@ledgerhq/react-native-hw-transport-ble'; +import type Transport from '@ledgerhq/hw-transport'; // type for the transporter +``` + +In a Web DAPP, take care that some browsers are not compatible (FireFox, ...), and that the Bluetooth is not working in all cases and in all operating systems. + +> [!NOTE] +> The last version of the Ledger Starknet APP (v1.1.1) only supports blind signing of the hash of your action. Sign only hashes from a code that you trust. + +For example, for a Node script : + +```typescript +import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'; +const myLedgerTransport = await TransportNodeHid.create(); +const myLedgerSigner = new LedgerSigner(myLedgerTransport, 0); +const pubK = await myLedgerSigner.getPubKey(); +const fullPubK = await myLedgerSigner.getFullPubKey(); +// ... +// deploy here an account related to this public key +// ... +const ledgerAccount = new Account(myProvider, ledger0addr, myLedgerSigner); +``` + +> [!IMPORTANT] +> The Ledger shall be connected, unlocked, with the Starknet internal APP activated, before launch of the script. + +Some complete examples : +A Node script : [here](https://github.com/PhilippeR26/starknet.js-workshop-typescript/blob/main/src/scripts/ledgerNano/5.testLedgerAccount.ts). +A test Web DAPP, to use in devnet-rs network : [here](https://github.com/PhilippeR26/Starknet-Ledger-Wallet). + +If you want to read the version of the Ledger Starknet APP : + +```typescript +const resp = await myLedgerTransport.send(Number('0x5a'), 0, 0, 0); +const appVersion = resp[0] + '.' + resp[1] + '.' + resp[2]; +console.log('version=', appVersion); +```