diff --git a/docs/diagrams/v2/core/transaction/transaction_id.md b/docs/diagrams/v2/core/transaction/transaction_id.md new file mode 100644 index 000000000..346b8ecb9 --- /dev/null +++ b/docs/diagrams/v2/core/transaction/transaction_id.md @@ -0,0 +1,197 @@ +# Transaction IDs May Collide + +The class [`Transaction`](../../../../../packages/core/src/transaction/Transaction.ts) +provides the computed property `Transaction.id()` to compute the transaction's identifier +as (l.242) + +```typescript +public get id(): Blake2b256 { + if (this.isSigned) { + return Blake2b256.of( + nc_utils.concatBytes( + this.getTransactionHash().bytes, + this.origin.bytes + ) + ); + } + throw new UnavailableTransactionField( + 'Transaction.id()', + 'not signed transaction: id unavailable', + {fieldName: 'id'} + ); +} +``` + +The following flowchart shows the inputs, methods and properties involved. + +```mermaid +flowchart TD + start((Start)) + stop(((stop))) + body[/TransactionBody/] + subgraph origin["Transaction.origin()"] + origin_txHash[["getTransactionHash()"]] + origin_signature[\signature\] + origin_recover[["Secp256k1.recover(txHash,signature)"]] + origin_address["Address.ofPublicKey(publicKey)"] + origin_recover --> origin_address + origin_signature --> origin_recover + + end + subgraph tx_hash["Transaction.getTransactionHash(gasPayer?)"] + txHash_encodeHash[Blake256.of] + txHash_encodePayer[Blake2b256.of] + txHash_encode[[encode]] + txHash_payer?{gasPayer?} + txHash_payer[/gasPayer/] + txHash_encode --> txHash_encodeHash + txHash_encodeHash --> txHash_payer? + txHash_payer? -. yes .-> txHash_encodePayer + txHash_payer --> txHash_encodePayer + end + subgraph tx_id["Transaction.id()"] + id_hash[Blake2b256.of] + id_origin[["origin()"]] + id_txHash[["getTransactionHash()"]] + id_origin --> id_hash + id_txHash --> id_hash + end + body --> txHash_encode + id_hash --> stop + origin_address --> id_origin + origin_txHash --> origin_recover + start --> body + txHash_encodePayer --> id_txHash + txHash_payer? -- no --> id_txHash + +``` + +## Not Delegated Transaction ID + +NCC claims + +_1. ... The transaction signature is not incorporated into the hash computation; only the transaction hash as well as +the +originator’s address are taken into account._ (p.12) + +but we observe **the origin's address is derived by the origin's signature because the computed property** +`Transaction.origin` (l.304). + +```typescript +public get origin(): Address +{ + if (this.signature !== undefined) { + return Address.ofPublicKey( + // Get the origin public key. + Secp256k1.recover( + this.getTransactionHash().bytes, + // Get the (r, s) of ECDSA digital signature without gas payer params. + this.signature.slice(0, Secp256k1.SIGNATURE_LENGTH) + ) + ); + } + throw new UnavailableTransactionField( + 'Transaction.origin()', + 'not signed transaction, no origin', + {fieldName: 'origin'} + ); +} +``` + +NCC clams + +_ ... if a transaction was signed by a signer multiple times (or in case of different +transactions with colliding getTransactionHash()), the resulting signed transactions would +have the same ID._ (p.13) + +We observe if the transaction is signed multiple times, it results the same id if the +`TransactionBody` object is equal and the origin's signature is the same. + +### Question - ask NCC + +Since the origin's address is a function of the origin's signature, **what is the difference +between to compute BLAKE2B256** + +* **from the transaction hash and the origin's signature**, and +* **from the transaction hash and the origin's address derived from its signature?** + +--- + +## Delegated Transaction ID + +NCC claims + +_2. In the case of delegated transactions, the function above also fails to take the gas +payer’s (aka delegator’s) signature and address into account._ (p.13) + +We observe the claim is true: in the `Transaction.id` computed property at l.246 + +```typescript +this.getTransactionHash().bytes +``` + +does not pass the `gasPayer` as argument, hence NCC is right to claim + +_... if a given to-be-delegated transaction were +signed by multiple different gas payers, the resulting IDs would all be equal (and would also +be equal to the ID of a non-delegated transaction)._ (p.13) + +We observe the `reserved` flag in the `Transaction` body participates in the computed hash, hence +a delegated transaction should result in a different id then a not delegated transaction, +this is the only input in the BLAKE2B256 hash to differentiate the id transactions of the same +`TransactionBody` between the cases when the transaction is delegated and when is not. Since a transaction must +include the origin signature and if is delegated, must include the `reserved` property and a gas payer's signature, +we do not think there is a real useful case to forge the `reserved` field, nevertheless we think ** + +NCC recommends + +_Consider modifying the id() function such that it includes the signer’s signature, and the +delegator’s (wrong name for the "gas payer") address and signature, if the transaction is delegated._ (p.13) + +We think this should be done, but we must evaluate what is done in Thor as well and if the Thor ID algorithm must change +following the NCC recommendations. + +NCC recommends + +_... reflect on whether the id() function should product a different digest than the +getTransactionHash() of a delegated transaction._ (p.13) + +**We think the id of delegated transaction should be different form its transaction's hash, we observe they are +different - matter of test - because the `Transaction.id` hashes the result of `Transaction.getTransactionHash` +concatenated with the transaction;s origin address.** + +--- + +### Question - ask the protocol team + +**The `gasPayer` signature or its derived address is never considered in the computation of the transaction hash!** + +* **What is the algorithm used in the protocol**? + * **Is the same algorithm implemented the in e SDK?** + * **The algorithm in the protocol does consider the `taxPayer` address and signature in the computation of the id?** + +**According to our tests, Thor and SDK produce the same id for the same transaction!** +--- + +## ID Equality + +We presume is correct to state equal +[`TransactionBody`](../../../../../packages/core/src/transaction/TransactionBody.d.ts) +instances signed by the same origin and gas payer should return the same id. +Since Thor rejects a transaction with a spent id, the above criteria should never be a problem; +Thor blockchain should never have two transaction with the same id, ids should be unique. +However, the `TransactionBody` class exposes the `nonce` property that is involved in the computation +of the transaction's hash. + +### Question - ask the protocol team + +The `Transaction.encode` method processes an object of the +[`TransactionBody`](../../../../../packages/core/src/transaction/TransactionBody.d.ts) +class, the class provides the `nonce` property +The `nonce` property should be different for each transaction even if +the other properties of the transaction are equal, however no assumption are made about `nonce`. +* **Who sets the `nonce` property?** + +--- + + diff --git a/docs/diagrams/v2/net/http/http.md b/docs/diagrams/v2/net/http/http.md new file mode 100644 index 000000000..0178766de --- /dev/null +++ b/docs/diagrams/v2/net/http/http.md @@ -0,0 +1,35 @@ +```mermaid +classDiagram + class FetchHttpClient { + baseURL: string + onRequest: OnRequest + onResponse: OnResponse + at(baseURL: string, onRequest: OnRequest, onResponse: OnResponse) FetchHttpClient + } + class HttpClient { + <<interface>> + get(httpPath: HttpPath, httpQuery: HttpQuery) Promise~Response~ + post(httpPath: HttpPath, httpQuery: HttpQuery, body?: unknown) Promise~Response~ + } + class HttpPath { + <<interface>> + path: string + } + class HttpQuery { + <<interface>> + query(): string; + } + class OnRequest { + <<callback>> + onRequest(request: Request) Request + } + class OnResponse{ + <<callback>> + onResponse(response: Response) Response + } + FetchHttpClient *--> OnRequest + FetchHttpClient *--> OnResponse + HttpClient <|.. FetchHttpClient + HttpPath <-- "get - post" HttpClient + HttpQuery <-- "get - post" HttpClient +``` diff --git a/docs/diagrams/v2/net/thor/accounts/accounts.md b/docs/diagrams/v2/net/thor/accounts/accounts.md new file mode 100644 index 000000000..bf9020525 --- /dev/null +++ b/docs/diagrams/v2/net/thor/accounts/accounts.md @@ -0,0 +1,218 @@ +```mermaid +classDiagram + namespace JS { + class Array~Type~ { + <<type>> + } + } + namespace http { + class HttpClient { + <<interface>> + get(httpPath: HttpPath) Promise~Response~ + post(httpPath: HttpPath, body?: unknown) Promise~Response~ + } + class HttpPath { + <<interface>> + path: string + } + } + namespace thor { + class ThorRequest~RequestClass~ { + <<interface>> + askTo(httpClient: HttpClient Promise~ThorResponse~ResponseClass~~; + } + class ThorResponse~ResponseClass~ { + <<interface>> + request: ThorRequest~RequestClass~ + response: ResponseClass + } + } + namespace transactions { + class Clause { + to: Address | null + value: VET + data: HexUInt + constructor(json: ClauseJSON) + toJSON() ClauseJSON + } + class ClauseJSON { + to: string | null + value: string + data: string + } + class Event { + address: Address + topics: ThorId[] + data: HexUInt + constructor(json: EventJSON) Event + toJSON() EventJSON + } + class EventJSON { + <<interface>> + address: string + topics: string[] + data: string + } + class Transfer { + sender: Address + recipient: Address + amount: VET + constructor(json: TransferJSON) Transfer + toJSON() TransferJSON + } + class TransferJSON { + <<interface>> + sender: string + recipient: string + amount: string + } + } + class ContractBytecode { + code HexUInt + constructor(json ContractBytecodeJSON) ContractBytecode + toJSON() ContractBytecodeJSON + } + class ContractBytecodeJSON { + <<interface>> + code: string + } + class ExecuteCodesRequest { + provedWork?: string + gasPayer?: Address + expiration?: UInt + blockRef?: BlockRef + clauses?: Clause[] + gas?: VTHO + gasPrice?: VTHO + caller?: Address + constructor(json: ExecuteCodesRequestJSON) ExecuteCodesRequest + toJSON() ExecuteCodesRequestJSON + } + class ExecuteCodesRequestJSON { + <<interface>> + provedWork?: string + gasPayer?: string + expiration?: number + blockRef?: string + clauses?: ClauseJSON[] + gas?: number + gasPrice?: string + caller?: string + } + class ExecuteCodeResponse { + data: HexUInt + events: Event[] + transfers: Transfer[] + gasUsed: VTHO + reverted: boolean + vmError: string + constructor(json: ExecuteCodeResponseJSON) + toJSON() ExecuteCodeResponseJSON + } + class ExecuteCodeResponseJSON { + <<interface>> + data: string + events: EventJSON[] + transfers: TransferJSON[] + gasUsed: number + reverted: boolean + vmError: string + } + class ExecuteCodesResponse { + constructor(json: ExecuteCodesResponseJSON) ExecuteCodesResponse + } + class ExecuteCodesResponseJSON { + <<interface>> + } + class GetAccountResponse { + balance: VET + energy: VTHO + hasCode: boolean + constructor(json: GetAccountResponseJSON) GetAccountResponse + } + class GetAccountResponseJSON { + <<interface>> + balance: string + energy: string + hasCode: boolean + } + class GetStorageResponse { + value: ThorId + constructor(json: GetStorageResponseJSON) GetStorageResponse + toJSON() GetStorageResponseJSON + } + class GetStorageResponseJSON { + <<interface>> + value: string + } + class InspectClauses { + PATH: HttpPath$ + askTo(httpClient: HttpClient) Promise~ThorResponse~ExecuteCodesResponse~~ + of(request: ExecuteCodesRequestJSON) InspectClauses$ + withRevision(revision: Revision) InspectClauses + } + class RetrieveAccountDetails { + path: RetrieveAccountDetailsPath + askTo(httpClient: HttpClient) Promise~ThorResponse~GetAccountResponse~~ + of(address: Address) RetrieveAccountDetails$ + } + class RetrieveAccountDetailsPath { + address: Address + } + class RetrieveContractBytecode { + path: RetrieveContractBytecodePath + askTo(httpClient: HttpClient) Promise~ThorResponse~ContractBytecode~~ + of(address: Address) RetrieveContractBytecode + } + class RetrieveContractBytecodePath { + address: Address + } + class RetrieveStoragePositionValue { + path: RetrieveStoragePositionValuePath + askTo(httpClient: HttpClient) Promise~ThorResponse~GetStorageResponse~~ + of(address: Address, key: BlockId) RetrieveStoragePositionValue$ + } + class RetrieveStoragePositionValuePath { + address: Address + key: BlockId + } + Clause --> "new - toJSON" ClauseJSON + ContractBytecode --> "new - toJSON" ContractBytecodeJSON + Event --> "new - toJSON" EventJSON + ExecuteCodeResponse *--> Event + ExecuteCodeResponse *--> Transfer + ExecuteCodeResponse --> "new - toJSON" ExecuteCodeResponseJSON + ExecuteCodeResponseJSON *--> EventJSON + ExecuteCodeResponseJSON *--> TransferJSON + ExecuteCodeResponseJSON --|> Array + ExecuteCodesRequest *--> Clause + ExecuteCodesRequest --> "new - toJSON" ExecuteCodesRequestJSON + ExecuteCodesRequestJSON *--> ClauseJSON + ExecuteCodesResponse *--> "ExecuteCodeResponseJSON[]" ExecuteCodeResponse + ExecuteCodesResponse --> "new - toJSON" ExecuteCodesResponseJSON + ExecuteCodesResponse --|> Array + ExecuteCodesResponseJSON *--> "ExecuteCodeResponseJSON[]" ExecuteCodeResponseJSON + GetAccountResponse --> "new - toJSON" GetAccountResponseJSON + GetStorageResponse --> "new - toJSON" GetStorageResponseJSON + HttpClient --> "get - post" HttpPath + HttpPath <|.. RetrieveAccountDetailsPath + HttpPath <|.. RetrieveContractBytecodePath + HttpPath <|.. RetrieveStoragePositionValuePath + InspectClauses --> "askTo" ExecuteCodesResponse + RetrieveAccountDetails *--> RetrieveAccountDetailsPath + RetrieveAccountDetails --> "askTo" GetAccountResponse + RetrieveContractBytecode *--> RetrieveContractBytecodePath + RetrieveContractBytecode --> "askTo" ContractBytecode + RetrieveStoragePositionValue *--> RetrieveStoragePositionValuePath + RetrieveStoragePositionValue --> "askTo" GetStorageResponse + ThorRequest <--* ThorResponse + ThorRequest <|.. InspectClauses + ThorRequest <|.. RetrieveAccountDetails + ThorRequest <|.. RetrieveContractBytecode + ThorRequest <|.. RetrieveStoragePositionValue + ThorResponse <-- "askTo" InspectClauses + ThorResponse <-- "askTo" RetrieveAccountDetails + ThorResponse <-- "askTo" RetrieveContractBytecode + ThorResponse <-- "askTo" RetrieveStoragePositionValue + Transfer --> "new - toJSON" TransferJSON +``` diff --git a/docs/diagrams/v2/net/thor/blocks/blocks.md b/docs/diagrams/v2/net/thor/blocks/blocks.md new file mode 100644 index 000000000..1b7c16e9a --- /dev/null +++ b/docs/diagrams/v2/net/thor/blocks/blocks.md @@ -0,0 +1,84 @@ +```mermaid +classDiagram + namespace http { + class HttpClient { + <<interface>> + get(httpPath: HttpPath) Promise~Response~ + post(httpPath: HttpPath, body?: unknown) Promise~Response~ + } + class HttpPath { + <<interface>> + path: string + } + } + namespace thor { + class ThorRequest~RequestClass~ { + <<interface>> + askTo(httpClient: HttpClient Promise~ThorResponse~ResponseClass~~; + } + class ThorResponse~ResponseClass~ { + <<interface>> + request: ThorRequest~RequestClass~ + response: ResponseClass + } + } + class RegularBlockResponse { + number: UInt + id: ThorId + size: UInt + parentID: ThorId + timestamp: bigint + gasLimit: VTHO + beneficiary: Address + gasUsed: VTHO + totalScore: UInt + txsRoot: ThorId + txsFeatures: UInt + stateRoot: ThorId + receiptsRoot: ThorId + com: boolean + signer: Address + isTrunk: boolean + isFinalized: boolean + transactions: ThorId[] + constructor(json: RegularBlockResponseJSON) RegularBlockResponse + toJSON() RegularBlockResponseJSON + } + class RegularBlockResponseJSON { + <<interface>> + number: number + id: string + size: number + parentID: string + timestamp: bigint + gasLimit: number + beneficiary: string + gasUsed: number + totalScore: number + txsRoot: string + txsFeatures: number + stateRoot: string + receiptsRoot: string + com: boolean + signer: string + isTrunk: boolean + isFinalized: boolean + transactions: string[] + } + class RetrieveBlock { + path: RetrieveBlockPath + askTo(httpClient: HttpClient) Promise~ThorResponse~RegularBlockResponse~~ + of(revision: Revision) RetrieveBlock$ + } + class RetrieveBlockPath { + revision: Revision + } + ThorResponse <-- "askTo" RetrieveBlock + ThorRequest <|.. RetrieveBlock + ThorRequest <--* ThorResponse + HttpPath <|.. RetrieveBlockPath + RetrieveBlock --> "askTo" RegularBlockResponse + RetrieveBlock *--> RetrieveBlockPath + RegularBlockResponse --> "new - toJSON" RegularBlockResponseJSON + HttpClient --> "get - post" HttpPath +``` diff --git a/docs/diagrams/v2/net/thor/debug/debug.md b/docs/diagrams/v2/net/thor/debug/debug.md new file mode 100644 index 000000000..f18a551b6 --- /dev/null +++ b/docs/diagrams/v2/net/thor/debug/debug.md @@ -0,0 +1,205 @@ +```mermaid +classDiagram + namespace JS { + class unknown { + <<type>> + } + } + namespace http { + class HttpClient { + <<interface>> + get(httpPath: HttpPath) Promise~Response~ + post(httpPath: HttpPath, body?: unknown) Promise~Response~ + } + class HttpPath { + <<interface>> + path: string + } + } + namespace thor { + class ThorRequest~RequestClass~ { + <<interface>> + askTo(httpClient: HttpClient Promise~ThorResponse~ResponseClass~~ + } + class ThorResponse~ResponseClass~ { + <<interface>> + request: ThorRequest~RequestClass~ + response: ResponseClass + } + } + class Bigram { + NAME: string$ + } + class Call { + NAME: string$ + } + class EvmDis { + NAME: string$ + } + class FourByte { + NAME: string$ + } + class Noop { + NAME: string$ + } + class Null { + NAME: string$ + } + class OpCount { + NAME: string$ + } + class PostDebugTracerCallRequest { + name?: TracerName + config?: unknown + value: VET + data: HexUInt + to?: Address + gas?: VTHO + gasPrice?: VTHO + caller?: Address + provedWork?: string + gasPayer?: Address + expiration?: UInt + blockRef?: BlockRef + constructor(json: PostDebugTracerCallRequestJSON) PostDebugTracerCallRequest + toJSON() PostDebugTracerCallRequestJSON + } + class PostDebugTracerCallRequestJSON { + <<interface>> + name?: string + config?: unknown + value: string + data: string + to?: string + gas?: number + gasPrice?: string + caller?: string + provedWork?: string + gasPayer?: string + expiration?: number + blockRef?: string + } + class PostDebugTracerRequest { + name?: TracerName + config?: unknown + target: string + constructor(json: PostDebugTracerRequestJSON) PostDebugTracerRequest + toJSON() PostDebugTracerRequestJSON + } + class PostDebugTracerRequestJSON { + <<interface>> + name?: string + config?: unknown + target: string + } + class Presate { + NAME: string$ + } + class RetrieveStorageRange { + PATH: HttpPath$ + request: StorageRangeOption + askTo(httpClient: HttpClient): Promise~ThorResponse~StorageRange~~ + of(request: StorageRangeOptionJSON) RetrieveStorageRange$ + } + class StorageRange { + nextKey?: ThorId + storage: unknown + constructor(json: StorageRangeJSON) StorageRange + toJSON() StorageRangeJSON + } + class StorageRangeJSON { + <<interface>> + nextKey?: string + storage: unknown + } + class StorageRangeOption { + address: Address + keyStart?: ThorId + maxResult?: UInt + target: string + } + class StorageRangeOptionJSON { + <<interface>> + address: string + keyStart?: string + maxResult?: number + target: string + } + class StructLogger { + NAME: string$ + } + class TraceCall { + PATH: HttpPath$ + request: PostDebugTracerCallRequest + askTo(httpClient: HttpClient): Promise~ThorResponse~unknown~~ + of(request: PostDebugTracerCallRequestJSON) TraceCall$ + } + class Tracer { + of(name: string): TracerName$ + } + class TracerName { + <<abstract>> + toString: string + } + class TraceTransactionClause { + PATH: HttpPath$ + request: PostDebugTracerRequest + askTo(httpClient: HttpClient): Promise~ThorResponse~unknown~~ + of(request: PostDebugTracerRequestJSON) TraceTransactionClause$ + } + class Trigram { + NAME: string$ + } + class Unigram { + NAME: string$ + } + Bigram <-- "of" Tracer + Call <-- "of" Tracer + EvmDis <-- "of" Tracer + FourByte <-- "of" Tracer + HttpClient --> "get - post" HttpPath + HttpPath <--* RetrieveStorageRange + HttpPath <--* TraceCall + HttpPath <--* TraceTransactionClause + Noop <-- "of" Tracer + Null <-- "of" Tracer + OpCount <-- "of" Tracer + PostDebugTracerCallRequest *--> TracerName + PostDebugTracerCallRequest --> "new - toJSON" PostDebugTracerCallRequestJSON + PostDebugTracerRequest *--> TracerName + PostDebugTracerRequest --> "new - toJSON" PostDebugTracerRequestJSON + Presate <-- "of" Tracer + Prestate <-- "of" Tracer + RetrieveStorageRange *--> StorageRangeOption + RetrieveStorageRange --> "askTo" StorageRange + RetrieveStorageRange --> "of" StorageRangeOptionJSON + StorageRange --> "new - toJSON" StorageRangeJSON + StorageRangeOption --> "new - toJSON" StorageRangeOptionJSON + StructLogger <-- "of" Tracer + ThorRequest <--* ThorResponse + ThorRequest <|.. RetrieveStorageRange + ThorRequest <|.. TraceCall + ThorRequest <|.. TraceTransactionClause + ThorResponse <-- "askTo" RetrieveStorageRange + ThorResponse <-- "askTo" TraceCall + TraceCall *--> PostDebugTracerCallRequest + TraceCall --> "askTo" unknown + TraceCall --> "of" PostDebugTracerCallRequestJSON + TraceTransactionClause *--> PostDebugTracerCallRequest + TraceTransactionClause --> "askTo" unknown + TraceTransactionClause --> "of" PostDebugTracerRequestJSON + TracerName <|-- Bigram + TracerName <|-- Call + TracerName <|-- EvmDis + TracerName <|-- FourByte + TracerName <|-- Noop + TracerName <|-- Null + TracerName <|-- OpCount + TracerName <|-- Presate + TracerName <|-- Prestate + TracerName <|-- StructLogger + TracerName <|-- Trigram + TracerName <|-- Unigram + Trigram <-- "of" Tracer + Unigram <-- "of" Tracer +``` diff --git a/docs/diagrams/v2/net/thor/logs/logs.md b/docs/diagrams/v2/net/thor/logs/logs.md new file mode 100644 index 000000000..752be6233 --- /dev/null +++ b/docs/diagrams/v2/net/thor/logs/logs.md @@ -0,0 +1,230 @@ +```mermaid +classDiagram + namespace JS { + class Array~Type~ { + <<type>> + } + } + namespace http { + class HttpClient { + <<interface>> + get(httpPath: HttpPath) Promise~Response~ + post(httpPath: HttpPath, body?: unknown) Promise~Response~ + } + class HttpPath { + <<interface>> + path: string + } + } + namespace thor { + class ThorRequest~RequestClass~ { + <<interface>> + askTo(httpClient: HttpClient Promise~ThorResponse~ResponseClass~~ + } + class ThorResponse~ResponseClass~ { + <<interface>> + request: ThorRequest~RequestClass~ + response: ResponseClass + } + } + class EventCriteria { + address?: Address + topic0?: ThorId + topic1?: ThorId + topic2?: ThorId + topic3?: ThorId + topic4?: ThorId + constructor(json: EventCriteriaJSON) EventCriteria + toJSON() EventCriteriaJSON + } + class EventCriteriaJSON { + <<interface>> + address?: string + topic0?: string + topic1?: string + topic2?: string + topic3?: string + topic4?: string + } + class EventLogFilterRequest { + range?: FilterRange + options?: FilterOptions + criteriaSet?: EventCriteria[] + order?: LogSort + constructor(json: EventLogFilterRequestJSON) EventLogFilterRequest + toJSON() EventLogFilterRequestJSON + } + class EventLogFilterRequestJSON { + range?: FilterRangeJSON + options?: FilterOptionsJSON + criteriaSet?: EventCriteriaJSON[] + order?: string + } + class EventLogResponse { + address: Address + topics: ThorId[] + data: HexUInt + meta: LogMeta + constructor(json: EventLogResponseJSON) EventLogResponse + toJSON() EventLogResponseJSON + } + class EventLogResponseJSON { + <<interface>> + address: string + topics: string[] + data: string + meta: LogMetaJSON + } + class EventLogsResponse { + constructor(json: EventLogsResponseJSON) EventLogsResponse + } + class EventLogsResponseJSON { + <<interface>> + } + class FilterOptions { + limit?: UInt + offset?: UInt + constructor(json: FilterOptionsJSON) FilterOptions + toJSON() FilterOptionsJSON + } + class FilterOptionsJSON { + limit?: number + offset?: number + } + class LogMeta { + blockID: BlockId + blockNumber: UInt + blockTimestamp: UInt + txID: TxId + txOrigin: Address + clauseIndex: UInt + constructor(json: LogMetaJSON) + toJSON() LogMetaJSON + } + class LogMetaJSON { + <<interface>> + blockID: string + blockNumber: number + blockTimestamp: number + txID: string + txOrigin: string + clauseIndex: number + } + class LogSort { + <<enumeration>> + asc: string + desc: string + } + class FilterRange { + unit?: FilterRangeUnits + from?: UInt + to?: UInt + constructor(json: FilterRangeJSON) FilterRange + toJSON() FilterRangeJSON + } + class FilterRangeJSON { + unit?: string + from?: number + to?: number + } + class FilterRangeUnits { + <<enumeration>> + block = 'block', + time = 'time' + } + class QuerySmartContractEvents { + PATH: HttpPath$ + request: EventLogFilterRequest + constructor(request: EventLogFilterRequest) QuerySmartContractEvents + askTo(httpClient: HttpClient) Promise~ThorResponse~EventLogsResponse~~ + static of(request: EventLogFilterRequestJSON) QuerySmartContractEvents$ + } + class QueryVETTransferEvents { + PATH: HttpPath$ + request: TransferLogFilterRequest + constructor(request: TransferLogFilterRequest) QueryVETTransferEvents + askTo(httpClient: HttpClient) Promise~ThorResponse~TransferLogsResponse~~ + of(request: TransferLogFilterRequestJSON) QueryVETTransferEvents$ + } + class TransferCriteria { + txOrigin?: Address + sender?: Address + recipient?: Address + constructor(json: TransferCriteriaJSON) TransferCriteria + toJSON() TransferCriteriaJSON + } + class TransferCriteriaJSON { + txOrigin?: string + sender?: string + recipient?: string + } + class TransferLogFilterRequest { + range?: FilterRange + options?: FilterOptions + criteriaSet?: TransferCriteria[] + order?: LogSort + constructor(json: TransferLogFilterRequestJSON) TransferLogFilterRequest + toJSON() TransferLogFilterRequestJSON + } + class TransferLogFilterRequestJSON { + range?: FilterRangeJSON + options?: FilterOptionsJSON + criteriaSet?: TransferCriteriaJSON[] + order?: string + } + class TransferLogResponse { + sender: Address + recipient: Address + amount: VET + meta: LogMeta + constructor(json: TransferLogResponseJSON) TransferLogResponse + toJSON() TransferLogResponseJSON + } + class TransferLogResponseJSON { + sender: string + recipient: string + amount: string + meta: LogMetaJSON + } + EventCriteria --> "new - toJSON" EventCriteriaJSON + EventLogFilterRequest *--> EventCriteria + EventLogFilterRequest *--> LogSort + EventLogFilterRequest --> "new - toJSON" EventLogFilterRequestJSON + EventLogFilterRequestJSON *--> EventCriteriaJSON + EventLogResponse *--> LogMeta + EventLogResponse --> "new - toJSON" EventLogResponseJSON + EventLogResponseJSON *--> LogMetaJSON + EventLogsResponse *--> EventLogResponse + EventLogsResponse --|> Array + EventLogsResponseJSON *--> EventLogResponseJSON + EventLogsResponseJSON --|> Array + FilterOptions --> "new - toJSON" FilterOptionsJSON + FilterRange *--> FilterRangeUnits + FilterRange --> "new - toJSON" FilterRangeJSON + HttpClient --> "get - post" HttpPath + HttpPath <--* QuerySmartContractEvents + HttpPath <--* QueryVETTransferEvents + LogMeta --> "new - toJSON" LogMetaJSON + QuerySmartContractEvents *--> EventLogFilterRequest + QuerySmartContractEvents --> "askTo" EventLogsResponse + QueryVETTransferEvents *--> TransferLogFilterRequest + QueryVETTransferEvents --> "askTo" TransferLogsResponse + ThorRequest <--* ThorResponse + ThorRequest <|.. QuerySmartContractEvents + ThorRequest <|.. QueryVETTransferEvents + ThorResponse <-- "askTo" QuerySmartContractEvents + ThorResponse <-- "askTo" QueryVETTransferEvents + TransferCriteria --> "new - toJSON" TransferCriteriaJSON + TransferLogFilterRequest *--> FilterOptions + TransferLogFilterRequest *--> FilterRange + TransferLogFilterRequest *--> LogSort + TransferLogFilterRequest *--> TransferCriteria + TransferLogFilterRequest --> "new - toJSON" TransferLogFilterRequestJSON + TransferLogResponse *--> LogMeta + TransferLogResponse --> "new - toJSON" TransferLogResponseJSON + TransferLogsResponse *--> TransferLogResponse + TransferLogsResponse --> "new - toJSON" TransferLogsResponseJSON + TransferLogsResponse --|> Array + TransferLogsResponseJSON *--> TransferLogResponseJSON + TransferLogsResponseJSON --|> Array +``` diff --git a/docs/diagrams/v2/net/thor/node/node.md b/docs/diagrams/v2/net/thor/node/node.md new file mode 100644 index 000000000..9e59c80be --- /dev/null +++ b/docs/diagrams/v2/net/thor/node/node.md @@ -0,0 +1,71 @@ +```mermaid +classDiagram + namespace JS { + class Array~Type~ { + <<type>> + } + } + namespace http { + class HttpClient { + <<interface>> + get(httpPath: HttpPath) Promise~Response~ + post(httpPath: HttpPath, body?: unknown) Promise~Response~ + } + class HttpPath { + <<interface>> + path: string + } + } + namespace thor { + class ThorRequest~RequestClass~ { + <<interface>> + askTo(httpClient: HttpClient Promise~ThorResponse~ResponseClass~~ + } + class ThorResponse~ResponseClass~ { + <<interface>> + request: ThorRequest~RequestClass~ + response: ResponseClass + } + } + class GetPeersResponse { + constructor(json: GetPeersResponseJSON) GetPeersResponse + } + class GetPeersResponseJSON { + <<interface>> + } + class PeerStat { + name: string + bestBlockID: BlockId + totalScore: UInt + peerID: string + netAddr: string + inbound: boolean + duration: UInt + constructor(json: PeerStatJSON) PeerStat + toJSON() PeerStatJSON + } + class PeerStatJSON { + name: string + bestBlockID: string + totalScore: number + peerID: string + netAddr: string + inbound: boolean + duration: number + } + class RetrieveConnectedPeers { + PATH: HttpPath$ + askTo(httpClient: HttpClient) Promise~ThorResponse~GetPeersResponse~~ + } + GetPeersResponse *--> PeerStat + GetPeersResponse --> "new - toJSON" GetPeersResponseJSON + GetPeersResponse --|> Array + GetPeersResponseJSON --|> Array + GetPeersResponseJSON *--> PeerStatJSON + HttpClient --> "get - post" HttpPath + HttpPath <--* RetrieveConnectedPeers + PeerStat --> "new - toJSON" PeerStatJSON + RetrieveConnectedPeers --> "askTo" GetPeersResponse + ThorRequest <-- "askTo" RetrieveConnectedPeers + ThorResponse <|.. RetrieveConnectedPeers +``` diff --git a/docs/diagrams/v2/net/thor/subscriptions/subscriptions.md b/docs/diagrams/v2/net/thor/subscriptions/subscriptions.md new file mode 100644 index 000000000..f9484eac0 --- /dev/null +++ b/docs/diagrams/v2/net/thor/subscriptions/subscriptions.md @@ -0,0 +1,215 @@ +```mermaid +classDiagram + namespace log { + class LogMeta { + blockID: BlockId + blockNumber: UInt + blockTimestamp: UInt + txID: TxId + txOrigin: Address + clauseIndex: UInt + constructor(json: LogMetaJSON) LogMeta + toJSON() LogMetaJSON + } + class LogMetaJSON { + blockID: string + blockNumber: number + blockTimestamp: number + txID: string + txOrigin: string + clauseIndex: number + } + } + namespace trasactions { + class TXID { + id: ThorId + constructor(json: TXIDJSON): TXID + toJSON() TXIDJSON + } + class TXIDJSON { + <<interface>> + id: string + } + } + namespace ws { + class WebSocketClient { + <<interface>> + baseURL: string + addMessageListener(listener: WebSocketListener) + close(): WebSocketClient + open(path: HttpPath): WebSocketClient + removeListener(listener: WebSocketListener): WebSocketClient + } + class WebSocketListener~EventType~ { + <<interface>> + onClose(event: Event) + onError(event: Event) + onMessage(event: MessageEvent~EventType~) + onOpen(event: Event) + } + } + class BeatsSubscription { + PATH: HttpPath$ + addMessageListener(listener: WebSocketListener~SubscriptionBeat2Response~) BeatsSubscription + at(wsc: WebSocketClient) BeatsSubscription$ + close() BeatsSubscription + open() BeatsSubscription + removeListener(listener: WebSocketListener~SubscriptionBeat2Response~) BeatsSubscription + } + class BlocksSubscription { + PATH: HttpPath$ + addMessageListener(listener: WebSocketListener~SubscriptionBlockResponse~) BlocksSubscription + at(wsc: WebSocketClient) BlocksSubscription$ + atPos(pos?: BlockId) + close() BlocksSubscription + open() BlocksSubscription + removeListener(listener: WebSocketListener~SubscriptionBlockResponse~) BlocksSubscription + } + class EventsSubscription { + PATH: HttpPath$ + addMessageListener(listener: WebSocketListener~SubscriptionEventResponse~) EventsSubscription + at(wsc: WebSocketClient) EventsSubscription$ + atPos(pos?: ThorId) EventsSubscription + close() EventsSubscription + open() EventsSubscription + removeListener(listener: WebSocketListener~SubscriptionEventResponse~) EventsSubscription + withContractAddress(contractAddress?: Address) EventsSubscription + withFilters(t0?: ThorId?, t1?: ThorId, t2: ThorId, t3: ThorId) EventsSubscription + } + class NewTransactionSubscription { + PATH: HttpPath$ + addMessageListener(listener: WebSocketListener~TXID~) NewTransactionSubscription + at(wsc: WebSocketClient) NewTransactionSubscription$ + close() NewTransactionSubscription + open() NewTransactionSubscription + } + class TransfersSubscription { + PATH: HttpPath$ + addMessageListener(listener: WebSocketListener~SubscriptionTransferResponse~) TransfersSubscription + at(wsc: WebSocketClient) TransfersSubscription + close() TransfersSubscription + open() TransfersSubscription + removeListener(listener: WebSocketListener~SubscriptionTransferResponse~) TransfersSubscription + } + class SubscriptionBeat2Response { + gasLimit: VTHO + obsolete: boolean + number: UInt + id: BlockId + parentID: BlockId + timestamp: UInt + txsFeatures: UInt + bloom: HexUInt + k: UInt + constructor(json: SubscriptionBeat2ResponseJSON): SubscriptionBeat2Response + toJSON() SubscriptionBeat2ResponseJSON + } + class SubscriptionBeat2ResponseJSON { + <<interface>> + gasLimit: number + obsolete: boolean + number: number + id: string + parentID: string + timestamp: number + txsFeatures: number + bloom: string + k: number + } + class SubscriptionBlockResponse { + number: UInt + id: BlockId + size: UInt + parentID: BlockId + timestamp: UInt + gasLimit: VTHO + beneficiary: Address + gasUsed: VTHO + totalScore: UInt + txsRoot: ThorId + txsFeatures: UInt + stateRoot: ThorId + receiptsRoot: ThorId + com: boolean + signer: Address + obsolete: boolean + transactions: TxId[] + } + class SubscriptionBlockResponseJSON { + <<interface>> + number: number + id: string + size: number + parentID: string + timestamp: number + gasLimit: number + beneficiary: string + gasUsed: number + totalScore: number + txsRoot: string + txsFeatures: number + stateRoot: string + receiptsRoot: string + com: boolean + signer: string + obsolete: boolean + transactions: string[] + } + class SubscriptionEventResponse { + address: Address; + topics: ThorId[]; + data: HexUInt; + obsolete: boolean; + meta: LogMeta; + constructor(json: SubscriptionEventResponseJSON) SubscriptionEventResponse + toJSON() SubscriptionEventResponseJSON + } + class SubscriptionEventResponseJSON { + <<interface>> + address: string + topics: string[] + data: string + obsolete: boolean + meta: LogMetaJSON + } + class SubscriptionTransferResponse { + sender: Address + recipient: Address + amount: VET + obsolete: boolean + meta: LogMeta + } + class SubscriptionTransferResponseJSON { + <<interface>> + sender: string + recipient: string + amount: string + obsolete: boolean + meta: LogMetaJSON + } + WebSocketClient <|.. BeatsSubscription + WebSocketClient <|.. BlocksSubscription + WebSocketClient <|.. EventsSubscription + WebSocketClient <|.. NewTransactionSubscription + WebSocketClient <|.. TransfersSubscription + WebSocketListener <|.. BeatsSubscription + WebSocketListener <|.. BlocksSubscription + WebSocketListener <|.. EventsSubscription + WebSocketListener <|.. NewTransactionSubscription + WebSocketListener <|.. TransfersSubscription + BeatsSubscription --> "onMessage" SubscriptionBeat2Response + BlocksSubscription --> "onMessage" SubscriptionBlockResponse + EventsSubscription --> "onMessage" SubscriptionEventResponse + NewTransactionSubscription --> "onMessage" TXID + TransfersSubscription --> "onMessage" SubscriptionTransferResponse + SubscriptionEventResponse *--> LogMeta + SubscriptionEventResponseJSON *--> LogMetaJSON + SubscriptionTransferResponse *--> LogMeta + SubscriptionTransferResponseJSON *-- LogMetaJSON + LogMeta --> "new - toJSON" LogMetaJSON + SubscriptionBeat2Response --> "new - toJSON" SubscriptionBeat2ResponseJSON + SubscriptionBlockResponse --> "new - toJSON" SubscriptionBlockResponseJSON + SubscriptionEventResponse --> "new - toJSON" SubscriptionEventResponseJSON + SubscriptionTransferResponse --> "new - toJSON" SubscriptionTransferResponseJSON + TXID --> "new - toJSON" TXIDJSON +``` diff --git a/docs/diagrams/v2/net/thor/thor.md b/docs/diagrams/v2/net/thor/thor.md new file mode 100644 index 000000000..fe72d1385 --- /dev/null +++ b/docs/diagrams/v2/net/thor/thor.md @@ -0,0 +1,18 @@ +```mermaid +classDiagram + class ThorNetworks { + <<enumeration>> + MAINNET: string + TESTNET: string + } + class ThorRequest~RequestClass~ { + <<interface>> + askTo(httpClient: HttpClient Promise~ThorResponse~ResponseClass~~; + } + class ThorResponse~ResponseClass~ { + <<interface>> + request: ThorRequest~RequestClass~ + response: ResponseClass + } + ThorRequest <--* ThorResponse +``` diff --git a/docs/diagrams/v2/net/thor/transactions/transactions.md b/docs/diagrams/v2/net/thor/transactions/transactions.md new file mode 100644 index 000000000..ae2d308f9 --- /dev/null +++ b/docs/diagrams/v2/net/thor/transactions/transactions.md @@ -0,0 +1,281 @@ +```mermaid +classDiagram + namespace http { + class HttpClient { + <<interface>> + get(httpPath: HttpPath) Promise~Response~ + post(httpPath: HttpPath, body?: unknown) Promise~Response~ + } + class HttpPath { + <<interface>> + path: string + } + class HttpQuery { + <<interface>> + query(): string; + } + } + namespace thor { + class ThorRequest~RequestClass~ { + <<interface>> + askTo(httpClient: HttpClient Promise~ThorResponse~ResponseClass~~ + of(txId: TxId) RetrieveTransactionByID$ + withHead(head?: BlockId) RetrieveTransactionByID + withPending(pending: boolean) RetrieveTransactionByID + } + class ThorResponse~ResponseClass~ { + <<interface>> + request: ThorRequest~RequestClass~ + response: ResponseClass + } + } + class Clause { + to?: Address | null + value: VET + data: HexUInt + constructor(json: ClauseJSON) Clause + toJSON() ClauseJSON + } + class ClauseJSON { + to?: string | null + value: string + data: string + } + class Event { + address: Address + topics: ThorId[] + data: HexUInt + constructor(json: EventJSON) Event + toJSON() EventJSON + } + class EventJSON { + address: string + topics: string[] + data: string + } + class GetRawTxResponse { + raw: HexUInt + meta: TxMeta + constructor(json: GetRawTxResponseJSON) GetRawTxResponse + toJSON() GetRawTxResponseJSON + } + class GetRawTxResponseJSON { + raw: string + meta: TxMetaJSON + } + class GetTxReceiptResponse { + meta: ReceiptMeta + constructor(json: GetTxReceiptResponseJSON) GetTxReceiptResponse + toJSON() GetTxReceiptResponseJSON + } + class GetTxReceiptResponseJSON { + meta: ReceiptMetaJSON + } + class GetTxResponse { + id: TxId + origin: Address + delegator: Address | null + size: UInt + chainTag: UInt + blockRef: BlockId + expiration: UInt + clauses: Clause[] + gasPriceCoef: UInt + gas: VTHO + dependsOn?: TxId + nonce: Nonce + meta: TxMeta + constructor(json: GetTxResponseJSON) GetTxResponse + toJSON() GetTxResponseJSON + } + class GetTxResponseJSON { + id: string + origin: string + delegator: string | null + size: number + chainTag: number + blockRef: string + expiration: number + clauses: ClauseJSON[] + gasPriceCoef: number + gas: number + dependsOn?: string + nonce: string + meta: TxMetaJSON + } + class TxMeta { + blockID: BlockId + blockNumber: UInt + blockTimestamp: bigint + constructor(json: TxMetaJSON) TxMeta + toJSON() TxMetaJSON + } + class TxMetaJSON { + blockID: string + blockNumber: number + blockTimestamp: bigint + } + class Receipt { + gasUsed: VTHO + gasPayer: Address + paid: VTHO + reward: VTHO + reverted: boolean + outputs: ReceiptOutput[] + constructor(json: ReceiptJSON) Receipt + toJSON() ReceiptJSON + } + class ReceiptJSON { + gasUsed: number + gasPayer: string + paid: string + reward: string + reverted: boolean + outputs: ReceiptOutputJSON[] + } + class ReceiptMeta { + txID: TxId + txOrigin: Address + constructor(json: ReceiptMetaJSON) ReceiptMeta + toJSON() ReceiptMetaJSON + } + class ReceiptMetaJSON { + txID: string + txOrigin: string + } + class ReceiptOutput { + contractAddress: Address + events: Event[] + transfers: Transfer[] + constructor(json: ReceiptOutputJSON) ReceiptOutput + toJSON() ReceiptOutputJSON + } + class ReceiptOutputJSON { + contractAddress: string + events: EventJSON[] + transfers: TransferJSON[] + } + class RetrieveRawTransactionByID { + path: RetrieveRawTransactionByIDPath + query: RetrieveRawTransactionByIDQuery + askTo(httpClient: HttpClient) Promise~ThorResponse~ + of(txId: TxId) RetrieveRawTransactionByID$ + withHead(head?: BlockId) RetrieveTransactionByID$ + withPending(pending: boolean) RetrieveTransactionByID$ + } + class RetrieveTransactionByID { + path: RetrieveTransactionByIDPath + query: RetrieveTransactionByIDQuery + askTo(httpClient: HttpClient) Promise~ThorRespons~GetTxResponse~~ + } + class RetrieveTransactionByIDPath { + txId: TxId + } + class RetrieveTransactionByIDQuery { + head?: BlockId + pending: boolean + } + class RetrieveTransactionReceipt { + path: RetrieveTransactionReceiptPath + query: RetrieveTransactionReceiptQuery + askTo(httpPath: HttpPath) Promise~ThorResponse~GetTxReceiptResponse~~ + of(txId: TxId) RetrieveTransactionReceipt$ + withHead(head?: BlockId) RetrieveTransactionReceipt + } + class RetrieveTransactionReceiptPath { + txId: TxId + } + class RetrieveTransactionReceiptQuery { + head?: BlockId + } + class SendTransaction { + PATH: HttpPath$ + encoded: Uint8Array + askTo(httpPath: HttpPath) Promise~ThorResponse~TXID~~ + of(encoded: Uint8Array) SendTransaction$ + } + class Transfer { + sender: Address + recipient: Address + amount: VET + constructor(json: TransferJSON) Transfer + toJSON() TransferJSON + } + class TransferJSON { + sender: string + recipient: string + amount: string + } + class TXID { + id: ThorId + constructor(json: TXIDJSON): TXID + toJSON() TXIDJSON + } + class TXIDJSON { + <<interface>> + id: string + } + class TxMeta { + blockID: BlockId + blockNumber: UInt + blockTimestamp: bigint + constructor(json: TxMetaJSON) TxMeta + toJSON() TxMetaJSON + } + class TxMetaJSON { + blockID: string + blockNumber: number + blockTimestamp: bigint + } + Clause --> "new - toJSON" ClauseJSON + Event --> "new - toJSON" EventJSON + GetRawTxResponse *--> TxMeta + GetRawTxResponse --> "new - toJSON" GetRawTxResponseJSON + GetRawTxResponse <-- "askTo" RetrieveRawTransactionByID + GetRawTxResponseJSON *--> TxMetaJSON + GetTxReceiptResponse *--> ReceiptMeta + GetTxReceiptResponse --> "new - toJSON" GetTxReceiptResponseJSON + GetTxReceiptResponseJSON *--> ReceiptMetaJSON + GetTxResponse *--> "*" Clause + GetTxResponse --> "new - toJSON" GetTxResponseJSON + GetTxResponseJSON *--> "*" ClauseJSON + HttpPath <--* SendTransaction + HttpPath <|.. RetrieveTransactionByIDPath + HttpPath <|.. RetrieveTransactionReceiptPath + HttpQuery <|.. RetrieveTransactionByIDQuery + HttpQuery <|.. RetrieveTransactionReceiptQuery + Receipt *--> "*" ReceiptOutput + Receipt --> "new - toJSON" ReceiptJSON + Receipt <|-- GetTxReceiptResponse + ReceiptJSON *--> "*" ReceiptOutputJSON + ReceiptJSON <|-- GetTxReceiptResponseJSON + ReceiptMeta --> "new - toJSON" ReceiptMetaJSON + ReceiptOutput *--> "*" Event + ReceiptOutput *--> "*" Transfer + ReceiptOutput --> "new - toJSON" ReceiptOutputJSON + ReceiptOutputJSON *--> "*" EventJSON + ReceiptOutputJSON *--> "*" TransferJSON + RetrieveRawTransactionByID *--> RetrieveRawTransactionByIDPath + RetrieveRawTransactionByID *--> RetrieveRawTransactionByIDQuery + RetrieveRawTransactionByID --> "askTo" GetRawTxResponse + RetrieveTransactionByID --> "askTo" GetTxResponse + RetrieveTransactionByIDPath <|-- RetrieveRawTransactionByIDPath + RetrieveTransactionByIDQuery <|-- RetrieveRawTransactionByIDQuery + RetrieveTransactionReceipt *--> RetrieveTransactionReceiptPath + RetrieveTransactionReceipt *--> RetrieveTransactionReceiptQuery + RetrieveTransactionReceipt --> "askTo" GetTxReceiptResponse + SendTransaction --> "askTo" TXID + ThorRequest <|.. RetrieveRawTransactionByID + ThorRequest <|.. RetrieveTransactionByID + ThorRequest <|.. RetrieveTransactionReceipt + ThorRequest <|.. SendTransaction + ThorResponse <-- "askTo" RetrieveRawTransactionByID + ThorResponse <-- "askTo" RetrieveTransactionByID + ThorResponse <-- "askTo" RetrieveTransactionReceipt + ThorResponse <-- "askTo" SendTransaction + Transfer --> "new - toJSON" TransferJSON + TXID --> "new - toJSON" TXIDJSON + TxMeta --> "new - toJSON" TxMetaJSON + TxMeta <|-- ReceiptMeta + TxMetaJSON <|-- ReceiptMetaJSON +``` diff --git a/docs/diagrams/v2/net/ws/ws.md b/docs/diagrams/v2/net/ws/ws.md new file mode 100644 index 000000000..a23307e2d --- /dev/null +++ b/docs/diagrams/v2/net/ws/ws.md @@ -0,0 +1,23 @@ +```mermaid +classDiagram + class MozillaWebSocketClient { + constructor(baseURL: string) MozillaWebSocketClient + } + class WebSocketClient { + <<interface>> + baseURL: string + addMessageListener(listener: WebSocketListener~EventType~) + close(): WebSocketClient + open(path: HttpPath): WebSocketClient + removeListener(listener: WebSocketListener<unknown>): WebSocketClient + } + class WebSocketListener~EventType~ { + <<interface>> + onClose(event: Event) + onError(event: Event) + onMessage(event: MessageEvent~EventType~) + onOpen(event: Event) + } + WebSocketClient <|.. MozillaWebSocketClient + WebSocketListener <--o WebSocketClient +``` diff --git a/docs/examples/transactions/full-flow-delegator-url.ts b/docs/examples/transactions/full-flow-delegator-url.ts index 71822faff..7889ed3fa 100644 --- a/docs/examples/transactions/full-flow-delegator-url.ts +++ b/docs/examples/transactions/full-flow-delegator-url.ts @@ -129,3 +129,4 @@ expect(delegatedSigned.isDelegated).toEqual(true); expect(txReceipt).toBeDefined(); expect(txReceipt?.gasUsed).toBe(gasResult.totalGas); expect(sendTransactionResult.id).toBe(txReceipt?.meta.txID); +console.log(txReceipt); diff --git a/packages/core/src/transaction/Transaction.ts b/packages/core/src/transaction/Transaction.ts index 3e36b9e13..d041c88d3 100644 --- a/packages/core/src/transaction/Transaction.ts +++ b/packages/core/src/transaction/Transaction.ts @@ -634,7 +634,7 @@ class Transaction { * @remarks Security auditable method, depends on * - {@link Address.ofPublicKey} * - {@link Secp256k1.isValidPrivateKey}; - * - {@link Secp256k1.sign}. + * - {@link Secp256k1.sign}.l */ public signAsSenderAndGasPayer( senderPrivateKey: Uint8Array, diff --git a/packages/core/src/vcdm/BlockId.ts b/packages/core/src/vcdm/BlockId.ts index e2bb39b41..d72d9aaaa 100644 --- a/packages/core/src/vcdm/BlockId.ts +++ b/packages/core/src/vcdm/BlockId.ts @@ -66,6 +66,7 @@ class BlockId extends HexUInt { * @throws {InvalidDataType} If the given expression is not a valid hexadecimal positive integer expression. */ public static of( + // eslint-disable-next-line sonarjs/use-type-alias exp: bigint | number | string | Uint8Array | HexUInt ): BlockId { try { @@ -107,4 +108,27 @@ class ThorId extends BlockId { } } -export { BlockId, ThorId }; +/** + * This class is an alias of {@link TxId} for back compatibility. + */ +class TxId extends BlockId { + /** + * Constructs an instance of the class with the specified transaction ID. + * + * @param {TxId} blockId - The unique identifier for the block. + */ + protected constructor(blockId: BlockId) { + super(blockId); + } + + /** + * See {@link BlockId.of}. + */ + public static of( + exp: bigint | number | string | Uint8Array | HexUInt + ): TxId { + return new TxId(BlockId.of(exp)); + } +} + +export { BlockId, ThorId, TxId }; diff --git a/packages/core/src/vcdm/Int.ts b/packages/core/src/vcdm/Int.ts new file mode 100644 index 000000000..4caf84c43 --- /dev/null +++ b/packages/core/src/vcdm/Int.ts @@ -0,0 +1,18 @@ +import { InvalidDataType } from '@vechain/sdk-errors'; + +class Int extends Number { + protected constructor(value: number) { + super(value); + } + + static of(exp: number): Int { + if (Number.isInteger(exp)) { + return new Int(exp); + } + throw new InvalidDataType('Int.of', 'not an integer expression', { + exp: `${exp}` + }); + } +} + +export { Int }; diff --git a/packages/core/src/vcdm/Nonce.ts b/packages/core/src/vcdm/Nonce.ts new file mode 100644 index 000000000..4b91232ae --- /dev/null +++ b/packages/core/src/vcdm/Nonce.ts @@ -0,0 +1,41 @@ +import { HexUInt } from './HexUInt'; +import { Hex } from './Hex'; +import { InvalidDataType } from '@vechain/sdk-errors'; + +class Nonce extends HexUInt { + private static readonly DIGITS = 8; + + protected constructor(huint: HexUInt) { + super(Hex.POSITIVE, huint.fit(Nonce.DIGITS).digits); + } + + public static isValid(exp: string): boolean { + return Hex.isValid(exp) && HexUInt.REGEX_HEXUINT_PREFIX.test(exp) + ? exp.length === Nonce.DIGITS + 2 + : exp.length === Nonce.DIGITS; + } + + public static isValid0x(exp: string): boolean { + return HexUInt.REGEX_HEXUINT_PREFIX.test(exp) && Nonce.isValid(exp); + } + + public static of( + exp: bigint | number | string | Uint8Array | HexUInt + ): Nonce { + try { + if (exp instanceof HexUInt) { + return new Nonce(exp); + } + return new Nonce(HexUInt.of(exp)); + } catch (e) { + throw new InvalidDataType( + 'Nonce.of', + 'not a Nonce expression', + { exp: `${exp}` }, // Needed to serialize bigint values. + e + ); + } + } +} + +export { Nonce }; diff --git a/packages/core/src/vcdm/UInt.ts b/packages/core/src/vcdm/UInt.ts new file mode 100644 index 000000000..fdd9b10c1 --- /dev/null +++ b/packages/core/src/vcdm/UInt.ts @@ -0,0 +1,23 @@ +import { Int } from './Int'; +import { InvalidDataType } from '@vechain/sdk-errors'; + +class UInt extends Int { + protected constructor(value: number) { + super(value); + } + + static of(exp: number): UInt { + if (exp >= 0 && Number.isInteger(exp)) { + return new UInt(exp); + } + throw new InvalidDataType( + 'UInt.of', + 'not an unsigned integer expression', + { + exp: `${exp}` + } + ); + } +} + +export { UInt }; diff --git a/packages/core/src/vcdm/index.ts b/packages/core/src/vcdm/index.ts index 9c12b8af5..29f3ed4fb 100644 --- a/packages/core/src/vcdm/index.ts +++ b/packages/core/src/vcdm/index.ts @@ -10,9 +10,12 @@ export * from './hash'; export * from './Hex'; export * from './HexInt'; export * from './HexUInt'; +export * from './Int'; export * from './Mnemonic'; +export * from './Nonce'; export * from './Quantity'; export * from './Revision'; export * from './BlockId'; export * from './Txt'; +export * from './UInt'; export type * from './VeChainDataModel'; diff --git a/packages/net/README.md b/packages/net/README.md new file mode 100644 index 000000000..a6db39615 --- /dev/null +++ b/packages/net/README.md @@ -0,0 +1 @@ +# @vechain/sdk-v2-network diff --git a/packages/net/eslint.config.mjs b/packages/net/eslint.config.mjs new file mode 100644 index 000000000..ca549b46e --- /dev/null +++ b/packages/net/eslint.config.mjs @@ -0,0 +1,19 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [ + ...baseConfig, + { + rules: { + "import/no-restricted-paths": ["error", { + zones: [{ + target: "./src", + + from: [ + "../errors" + ], + + message: "Please import using @vechain/sdk-<the-module>", + }], + }], + }, + }]; diff --git a/packages/net/jest.browser-setup.js b/packages/net/jest.browser-setup.js new file mode 100644 index 000000000..f5a510b90 --- /dev/null +++ b/packages/net/jest.browser-setup.js @@ -0,0 +1,18 @@ +require('whatwg-fetch'); + +const fetchMock = require('jest-fetch-mock'); + +// Don't auto-enable mocks +fetchMock.dontMock(); + +// Jest configuration for WebSocket mocking based on environment +if (typeof window === 'undefined') { + // Running in Node.js environment + jest.mock('ws', () => require('ws')); +} else { + // Running in browser environment + global.WebSocket = window.WebSocket; +} + +// Make fetch global +global.fetch = fetch; \ No newline at end of file diff --git a/packages/net/jest.config.browser.js b/packages/net/jest.config.browser.js new file mode 100644 index 000000000..349145031 --- /dev/null +++ b/packages/net/jest.config.browser.js @@ -0,0 +1,10 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: '../../customEnv.js', + setupFiles: ['./jest.browser-setup.js'], + coverageReporters: ['html', 'lcov', 'json'], + runner: 'groups', + reporters: ['default', 'jest-junit'], + workerThreads: true +}; diff --git a/packages/net/jest.config.js b/packages/net/jest.config.js new file mode 100644 index 000000000..27eab725f --- /dev/null +++ b/packages/net/jest.config.js @@ -0,0 +1,23 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +// Coverage threshold would apply to yarn test, not yarn test:unit +const isUnitTest = process.env.UNIT; + +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + coverageReporters: ['html', 'lcov', 'json'], + runner: 'groups', + reporters: ['default', 'jest-junit'], + workerThreads: true, + coverageThreshold: + isUnitTest !== 'true' + ? { + global: { + branches: 98, + functions: 99, + lines: 99, + statements: 99 + } + } + : undefined +}; diff --git a/packages/net/package.json b/packages/net/package.json new file mode 100644 index 000000000..481d1f382 --- /dev/null +++ b/packages/net/package.json @@ -0,0 +1,49 @@ +{ + "name": "@vechain/sdk-v2-network", + "version": "1.0.0-rc.6", + "description": "This module serves connects decentralized applications (dApps) the VeChainThor blockchain", + "author": "VeChain Foundation", + "license": "MIT", + "homepage": "https://github.com/vechain/vechain-sdk-js", + "repository": { + "type": "git", + "url": "github:vechain/vechain-sdk-js" + }, + "keywords": [ + "VeChain" + ], + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist", + "src", + "package.json", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "rm -rf ./dist && tsup-node src/index.ts --format cjs,esm --dts", + "check:circular-dependencies": "npx madge --json --circular --extensions ts src | jq '. | length' | awk '{if($1 > 15) exit 1}'", + "lint": "eslint", + "format": "prettier --write src/**/*.ts tests/**/*.ts solo-seeding/**/*.ts", + "start-thor-solo": "echo 'Starting thor solo node ...' && docker compose -f ../../docker-compose.thor.yml up -d --wait && echo '\nThor solo node started ...'", + "stop-thor-solo": "echo 'Stopping thor solo node ...' && docker compose -f ../../docker-compose.thor.yml down && echo 'Thor solo node stopped ...'", + "test:unit": "rm -rf ./coverageUnit && UNIT=true jest --coverage --coverageDirectory=coverageUnit --group=unit", + "test:integration": "rm -rf ./coverageIntegration && jest --coverage --coverageDirectory=coverageIntegration --group=integration", + "test:integration:solo": "(yarn start-thor-solo && yarn test:integration && yarn stop-thor-solo) || yarn stop-thor-solo", + "test:browser": "rm -rf ./coverage && jest --coverage --coverageDirectory=coverage --group=integration --group=unit --config ./jest.config.browser.js", + "test": "rm -rf ./coverage && jest --coverage --coverageDirectory=coverage --group=integration --group=unit", + "test:solo": "(yarn start-thor-solo && yarn test && yarn stop-thor-solo) || yarn stop-thor-solo", + "test:browser:solo": "(yarn start-thor-solo && yarn test:browser && yarn stop-thor-solo) || yarn stop-thor-solo" + }, + "dependencies": { + "@vechain/sdk-core": "1.0.0-rc.6", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/ws": "^8.5.13", + "jest-fetch-mock": "^3.0.3", + "whatwg-fetch": "^3.6.20" + } +} diff --git a/packages/net/src/http/FetchHttpClient.ts b/packages/net/src/http/FetchHttpClient.ts new file mode 100644 index 000000000..e699e59ac --- /dev/null +++ b/packages/net/src/http/FetchHttpClient.ts @@ -0,0 +1,76 @@ +import { type HttpClient } from './HttpClient'; +import { type HttpPath } from './HttpPath'; +import { type HttpQuery } from './HttpQuery'; + +class FetchHttpClient implements HttpClient { + private static readonly PATH_SEPARATOR = '/'; + + public readonly baseURL: string; + + private readonly onRequest: (request: Request) => Request; + + private readonly onResponse: (response: Response) => Response; + + constructor( + baseURL: string, + onRequest: (request: Request) => Request, + onResponse: (response: Response) => Response + ) { + this.baseURL = baseURL.endsWith(FetchHttpClient.PATH_SEPARATOR) + ? baseURL.substring(0, baseURL.length - 1) + : baseURL; + this.onRequest = onRequest; + this.onResponse = onResponse; + } + + static at( + baseURL: string, + onRequest: (request: Request) => Request = (request) => request, + onResponse: (response: Response) => Response = (response) => response + ): FetchHttpClient { + return new FetchHttpClient(baseURL, onRequest, onResponse); + } + + async get( + httpPath: HttpPath = { + path: '' + }, + httpQuery: HttpQuery = { + query: '' + } + ): Promise<Response> { + const path = httpPath.path.startsWith(FetchHttpClient.PATH_SEPARATOR) + ? httpPath.path.substring(1) + : httpPath.path; + const request = new Request( + `${this.baseURL}${FetchHttpClient.PATH_SEPARATOR}${path}${httpQuery.query}` + ); + const response = await fetch(this.onRequest(request)); + return this.onResponse(response); + } + + async post( + httpPath: HttpPath = { + path: '' + }, + httpQuery: HttpQuery = { + query: '' + }, + body?: unknown + ): Promise<Response> { + const path = httpPath.path.startsWith(FetchHttpClient.PATH_SEPARATOR) + ? httpPath.path.substring(1) + : httpPath.path; + const request = new Request( + `${this.baseURL}${FetchHttpClient.PATH_SEPARATOR}${path}${httpQuery.query}`, + { + body: JSON.stringify(body), + method: 'POST' + } + ); + const response = await fetch(this.onRequest(request)); + return this.onResponse(response); + } +} + +export { FetchHttpClient }; diff --git a/packages/net/src/http/HttpClient.ts b/packages/net/src/http/HttpClient.ts new file mode 100644 index 000000000..aea4e43e2 --- /dev/null +++ b/packages/net/src/http/HttpClient.ts @@ -0,0 +1,12 @@ +import { type HttpPath } from './HttpPath'; +import { type HttpQuery } from './HttpQuery'; + +export interface HttpClient { + get: (httpPath: HttpPath, httpQuery: HttpQuery) => Promise<Response>; + + post: ( + httpPath: HttpPath, + httpQuery: HttpQuery, + body?: unknown + ) => Promise<Response>; +} diff --git a/packages/net/src/http/HttpPath.ts b/packages/net/src/http/HttpPath.ts new file mode 100644 index 000000000..487f63980 --- /dev/null +++ b/packages/net/src/http/HttpPath.ts @@ -0,0 +1,3 @@ +export interface HttpPath { + get path(): string; +} diff --git a/packages/net/src/http/HttpQuery.ts b/packages/net/src/http/HttpQuery.ts new file mode 100644 index 000000000..3e0a56bc9 --- /dev/null +++ b/packages/net/src/http/HttpQuery.ts @@ -0,0 +1,3 @@ +export interface HttpQuery { + get query(): string; +} diff --git a/packages/net/src/http/index.ts b/packages/net/src/http/index.ts new file mode 100644 index 000000000..fca06d72a --- /dev/null +++ b/packages/net/src/http/index.ts @@ -0,0 +1,4 @@ +export * from './FetchHttpClient'; +export * from './HttpClient'; +export * from './HttpPath'; +export * from './HttpQuery'; diff --git a/packages/net/src/index.ts b/packages/net/src/index.ts new file mode 100644 index 000000000..9624102dd --- /dev/null +++ b/packages/net/src/index.ts @@ -0,0 +1,2 @@ +export * from './http'; +export * from './thor'; diff --git a/packages/net/src/thor/ThorNetworks.ts b/packages/net/src/thor/ThorNetworks.ts new file mode 100644 index 000000000..0d570c137 --- /dev/null +++ b/packages/net/src/thor/ThorNetworks.ts @@ -0,0 +1,7 @@ +enum ThorNetworks { + MAINNET = 'https://mainnet.vechain.org/', + SOLONET = 'http://localhost:8669/', + TESTNET = 'https://testnet.vechain.org/' +} + +export { ThorNetworks }; diff --git a/packages/net/src/thor/ThorRequest.ts b/packages/net/src/thor/ThorRequest.ts new file mode 100644 index 000000000..76ad64746 --- /dev/null +++ b/packages/net/src/thor/ThorRequest.ts @@ -0,0 +1,11 @@ +import { type HttpClient } from '../http'; +import { type ThorResponse } from './ThorResponse'; + +export interface ThorRequest< + RequestClass extends ThorRequest<RequestClass, ResponseClass>, + ResponseClass +> { + askTo: ( + httpClient: HttpClient + ) => Promise<ThorResponse<RequestClass, ResponseClass>>; +} diff --git a/packages/net/src/thor/ThorResponse.ts b/packages/net/src/thor/ThorResponse.ts new file mode 100644 index 000000000..e925587c2 --- /dev/null +++ b/packages/net/src/thor/ThorResponse.ts @@ -0,0 +1,9 @@ +import { type ThorRequest } from './ThorRequest'; + +export interface ThorResponse< + RequestClass extends ThorRequest<RequestClass, ResponseClass>, + ResponseClass +> { + request: RequestClass; + response: ResponseClass; +} diff --git a/packages/net/src/thor/accounts/ContractBytecode.ts b/packages/net/src/thor/accounts/ContractBytecode.ts new file mode 100644 index 000000000..a6cac2c9b --- /dev/null +++ b/packages/net/src/thor/accounts/ContractBytecode.ts @@ -0,0 +1,21 @@ +import { HexUInt } from '@vechain/sdk-core'; + +interface ContractBytecodeJSON { + code: string; +} + +class ContractBytecode { + readonly code: HexUInt; + + constructor(json: ContractBytecodeJSON) { + this.code = HexUInt.of(json.code); + } + + toJSON(): ContractBytecodeJSON { + return { + code: this.code.toString() + }; + } +} + +export { ContractBytecode, type ContractBytecodeJSON }; diff --git a/packages/net/src/thor/accounts/ExecuteCodesRequest.ts b/packages/net/src/thor/accounts/ExecuteCodesRequest.ts new file mode 100644 index 000000000..0ea2860d3 --- /dev/null +++ b/packages/net/src/thor/accounts/ExecuteCodesRequest.ts @@ -0,0 +1,71 @@ +import { Clause, type ClauseJSON } from '../transactions'; +import { UInt } from '../../../../core/src'; +import { Address, BlockRef, Units, VTHO } from '@vechain/sdk-core'; + +class ExecuteCodesRequest { + readonly provedWork?: string; + readonly gasPayer?: Address; + readonly expiration?: UInt; + readonly blockRef?: BlockRef; + readonly clauses?: Clause[]; + readonly gas?: VTHO; + readonly gasPrice?: VTHO; + readonly caller?: Address; + + constructor(json: ExecuteCodesRequestJSON) { + this.provedWork = json.provedWork; + this.gasPayer = + json.gasPayer === undefined ? undefined : Address.of(json.gasPayer); + this.expiration = + json.expiration === undefined + ? undefined + : UInt.of(json.expiration); + this.blockRef = + json.blockRef === undefined + ? undefined + : BlockRef.of(json.blockRef); + this.clauses = + json.clauses === undefined + ? undefined + : json.clauses.map( + (clauseJSON: ClauseJSON): Clause => new Clause(clauseJSON) + ); + this.gas = + json.gas === undefined ? undefined : VTHO.of(json.gas, Units.wei); + this.gasPrice = + json.gasPrice === undefined + ? undefined + : VTHO.of(json.gasPrice, Units.wei); + this.caller = + json.caller === undefined ? undefined : Address.of(json.caller); + } + + toJSON(): ExecuteCodesRequestJSON { + return { + provedWork: this.provedWork, + gasPayer: this.gasPayer?.toString(), + expiration: this.expiration?.valueOf(), + blockRef: this.blockRef?.toString(), + clauses: this.clauses?.map((clause: Clause) => clause.toJSON()), + gas: this.gas === undefined ? undefined : Number(this.gas.wei), + gasPrice: + this.gasPrice === undefined + ? undefined + : this.gasPrice.wei.toString(), + caller: this.caller?.toString() + } satisfies ExecuteCodesRequestJSON; + } +} + +class ExecuteCodesRequestJSON { + provedWork?: string; + gasPayer?: string; + expiration?: number; + blockRef?: string; + clauses?: ClauseJSON[]; + gas?: number; + gasPrice?: string; + caller?: string; +} + +export { ExecuteCodesRequest, type ExecuteCodesRequestJSON }; diff --git a/packages/net/src/thor/accounts/ExecuteCodesResponse.ts b/packages/net/src/thor/accounts/ExecuteCodesResponse.ts new file mode 100644 index 000000000..bdd547304 --- /dev/null +++ b/packages/net/src/thor/accounts/ExecuteCodesResponse.ts @@ -0,0 +1,73 @@ +import { + Event, + type EventJSON, + Transfer, + type TransferJSON +} from '../transactions'; +import { HexUInt, VTHO } from '@vechain/sdk-core'; + +class ExecuteCodeResponse { + readonly data: HexUInt; + readonly events: Event[]; + readonly transfers: Transfer[]; + readonly gasUsed: VTHO; + readonly reverted: boolean; + readonly vmError: string; + + constructor(json: ExecuteCodeResponseJSON) { + this.data = HexUInt.of(json.data); + this.events = json.events.map( + (eventJSON: EventJSON): Event => new Event(eventJSON) + ); + this.transfers = json.transfers.map( + (transferJSON: TransferJSON): Transfer => new Transfer(transferJSON) + ); + this.gasUsed = VTHO.of(json.gasUsed); + this.reverted = json.reverted; + this.vmError = json.vmError; + } + + toJSON(): ExecuteCodeResponseJSON { + return { + data: this.data.toString(), + events: this.events.map( + (event: Event): EventJSON => event.toJSON() + ), + transfers: this.transfers.map( + (transfer: Transfer): TransferJSON => transfer.toJSON() + ), + gasUsed: Number(this.gasUsed.wei), + reverted: this.reverted, + vmError: this.vmError + } satisfies ExecuteCodeResponseJSON; + } +} + +class ExecuteCodesResponse extends Array<ExecuteCodeResponse> { + constructor(json: ExecuteCodesResponseJSON) { + super( + ...json.map( + (json: ExecuteCodeResponseJSON): ExecuteCodeResponse => + new ExecuteCodeResponse(json) + ) + ); + } +} + +interface ExecuteCodeResponseJSON { + data: string; + events: EventJSON[]; + transfers: TransferJSON[]; + gasUsed: number; + reverted: boolean; + vmError: string; +} + +interface ExecuteCodesResponseJSON extends Array<ExecuteCodeResponseJSON> {} + +export { + ExecuteCodeResponse, + ExecuteCodesResponse, + type ExecuteCodeResponseJSON, + type ExecuteCodesResponseJSON +}; diff --git a/packages/net/src/thor/accounts/GetAccountResponse.ts b/packages/net/src/thor/accounts/GetAccountResponse.ts new file mode 100644 index 000000000..fd4fc2ee8 --- /dev/null +++ b/packages/net/src/thor/accounts/GetAccountResponse.ts @@ -0,0 +1,29 @@ +import { Quantity, VET, VTHO } from '@vechain/sdk-core'; + +class GetAccountResponse { + readonly balance: VET; + readonly energy: VTHO; + readonly hasCode: boolean; + + constructor(json: GetAccountResponseJSON) { + this.balance = VET.of(json.balance); + this.energy = VTHO.of(json.energy); + this.hasCode = json.hasCode; + } + + toJSON(): GetAccountResponseJSON { + return { + balance: Quantity.of(this.balance.wei).toString(), + energy: Quantity.of(this.energy.wei).toString(), + hasCode: this.hasCode + }; + } +} + +interface GetAccountResponseJSON { + balance: string; + energy: string; + hasCode: boolean; +} + +export { GetAccountResponse, type GetAccountResponseJSON }; diff --git a/packages/net/src/thor/accounts/GetStorageResponse.ts b/packages/net/src/thor/accounts/GetStorageResponse.ts new file mode 100644 index 000000000..406ba6b2d --- /dev/null +++ b/packages/net/src/thor/accounts/GetStorageResponse.ts @@ -0,0 +1,21 @@ +import { ThorId } from '@vechain/sdk-core'; + +class GetStorageResponse { + readonly value: ThorId; + + constructor(json: GetStorageResponseJSON) { + this.value = ThorId.of(json.value); + } + + toJSON(): GetStorageResponseJSON { + return { + value: this.value.toString() + } satisfies GetStorageResponseJSON; + } +} + +interface GetStorageResponseJSON { + value: string; +} + +export { GetStorageResponse, type GetStorageResponseJSON }; diff --git a/packages/net/src/thor/accounts/InspectClauses.ts b/packages/net/src/thor/accounts/InspectClauses.ts new file mode 100644 index 000000000..c976271d9 --- /dev/null +++ b/packages/net/src/thor/accounts/InspectClauses.ts @@ -0,0 +1,71 @@ +import { + ExecuteCodesResponse, + type ExecuteCodesResponseJSON +} from './ExecuteCodesResponse'; +import { type ThorRequest } from '../ThorRequest'; +import { type HttpClient, type HttpPath, type HttpQuery } from '../../http'; +import { type ThorResponse } from '../ThorResponse'; +import { + ExecuteCodesRequest, + type ExecuteCodesRequestJSON +} from './ExecuteCodesRequest'; +import { Revision } from '@vechain/sdk-core'; + +class InspectClauses + implements ThorRequest<InspectClauses, ExecuteCodesResponse> +{ + static readonly PATH: HttpPath = { path: '/accounts/*' }; + + private readonly query: InspectClauseQuery; + + private readonly request: ExecuteCodesRequest; + + constructor(query: InspectClauseQuery, request: ExecuteCodesRequest) { + this.query = query; + this.request = request; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<InspectClauses, ExecuteCodesResponse>> { + const response = await httpClient.post( + InspectClauses.PATH, + this.query, + this.request.toJSON() + ); + const responseBody = + (await response.json()) as ExecuteCodesResponseJSON; + return { + request: this, + response: new ExecuteCodesResponse(responseBody) + }; + } + + static of(request: ExecuteCodesRequestJSON): InspectClauses { + return new InspectClauses( + new InspectClauseQuery(Revision.BEST), + new ExecuteCodesRequest(request) + ); + } + + withRevision(revision: Revision = Revision.BEST): InspectClauses { + return new InspectClauses( + new InspectClauseQuery(revision), + this.request + ); + } +} + +class InspectClauseQuery implements HttpQuery { + readonly revision: Revision; + + constructor(revision: Revision) { + this.revision = revision; + } + + get query(): string { + return `?revision=${this.revision}`; + } +} + +export { InspectClauses }; diff --git a/packages/net/src/thor/accounts/RetrieveAccountDetails.ts b/packages/net/src/thor/accounts/RetrieveAccountDetails.ts new file mode 100644 index 000000000..d6bc13dcf --- /dev/null +++ b/packages/net/src/thor/accounts/RetrieveAccountDetails.ts @@ -0,0 +1,49 @@ +import { type HttpClient, type HttpPath } from '../../http'; +import { type ThorRequest } from '../ThorRequest'; +import { type ThorResponse } from '../ThorResponse'; +import { + GetAccountResponse, + type GetAccountResponseJSON +} from './GetAccountResponse'; +import { type Address } from '@vechain/sdk-core'; + +class RetrieveAccountDetails + implements ThorRequest<RetrieveAccountDetails, GetAccountResponse> +{ + readonly path: RetrieveAccountDetailsPath; + + constructor(path: RetrieveAccountDetailsPath) { + this.path = path; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<RetrieveAccountDetails, GetAccountResponse>> { + const response = await httpClient.get(this.path, { query: '' }); + const responseBody = (await response.json()) as GetAccountResponseJSON; + return { + request: this, + response: new GetAccountResponse(responseBody) + }; + } + + static of(address: Address): RetrieveAccountDetails { + return new RetrieveAccountDetails( + new RetrieveAccountDetailsPath(address) + ); + } +} + +class RetrieveAccountDetailsPath implements HttpPath { + readonly address: Address; + + constructor(address: Address) { + this.address = address; + } + + get path(): string { + return `/accounts/${this.address}`; + } +} + +export { RetrieveAccountDetails, RetrieveAccountDetailsPath }; diff --git a/packages/net/src/thor/accounts/RetrieveContractBytecode.ts b/packages/net/src/thor/accounts/RetrieveContractBytecode.ts new file mode 100644 index 000000000..7bf4b293f --- /dev/null +++ b/packages/net/src/thor/accounts/RetrieveContractBytecode.ts @@ -0,0 +1,49 @@ +import { + ContractBytecode, + type ContractBytecodeJSON +} from './ContractBytecode'; +import { type ThorRequest } from '../ThorRequest'; +import { type HttpClient, type HttpPath } from '../../http'; +import type { Address } from '@vechain/sdk-core'; +import { type ThorResponse } from '../ThorResponse'; + +class RetrieveContractBytecode + implements ThorRequest<RetrieveContractBytecode, ContractBytecode> +{ + readonly path: RetrieveContractBytecodePath; + + constructor(path: RetrieveContractBytecodePath) { + this.path = path; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<RetrieveContractBytecode, ContractBytecode>> { + const response = await httpClient.get(this.path, { query: '' }); + const responseBody = (await response.json()) as ContractBytecodeJSON; + return { + request: this, + response: new ContractBytecode(responseBody) + }; + } + + static of(address: Address): RetrieveContractBytecode { + return new RetrieveContractBytecode( + new RetrieveContractBytecodePath(address) + ); + } +} + +class RetrieveContractBytecodePath implements HttpPath { + readonly address: Address; + + constructor(address: Address) { + this.address = address; + } + + get path(): string { + return `/accounts/${this.address}/code`; + } +} + +export { RetrieveContractBytecode, RetrieveContractBytecodePath }; diff --git a/packages/net/src/thor/accounts/RetrieveStoragePositionValue.ts b/packages/net/src/thor/accounts/RetrieveStoragePositionValue.ts new file mode 100644 index 000000000..19ed82d6a --- /dev/null +++ b/packages/net/src/thor/accounts/RetrieveStoragePositionValue.ts @@ -0,0 +1,52 @@ +import { type HttpClient, type HttpPath } from '../../http'; +import { type Address, type BlockId } from '@vechain/sdk-core'; +import { type ThorRequest } from '../ThorRequest'; +import { + GetStorageResponse, + type GetStorageResponseJSON +} from './GetStorageResponse'; +import { type ThorResponse } from '../ThorResponse'; + +class RetrieveStoragePositionValue + implements ThorRequest<RetrieveStoragePositionValue, GetStorageResponse> +{ + readonly path: RetrieveStoragePositionValuePath; + + constructor(path: RetrieveStoragePositionValuePath) { + this.path = path; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<RetrieveStoragePositionValue, GetStorageResponse>> { + const response = await httpClient.get(this.path, { query: '' }); + const responseBody = (await response.json()) as GetStorageResponseJSON; + return { + request: this, + response: new GetStorageResponse(responseBody) + }; + } + + static of(address: Address, key: BlockId): RetrieveStoragePositionValue { + return new RetrieveStoragePositionValue( + new RetrieveStoragePositionValuePath(address, key) + ); + } +} + +class RetrieveStoragePositionValuePath implements HttpPath { + readonly address: Address; + + readonly key: BlockId; + + constructor(address: Address, key: BlockId) { + this.address = address; + this.key = key; + } + + get path(): string { + return `/accounts/${this.address}/storage/${this.key}`; + } +} + +export { RetrieveStoragePositionValue, RetrieveStoragePositionValuePath }; diff --git a/packages/net/src/thor/accounts/index.ts b/packages/net/src/thor/accounts/index.ts new file mode 100644 index 000000000..bed432f86 --- /dev/null +++ b/packages/net/src/thor/accounts/index.ts @@ -0,0 +1,9 @@ +export * from './ContractBytecode'; +export * from './ExecuteCodesRequest'; +export * from './ExecuteCodesResponse'; +export * from './GetAccountResponse'; +export * from './InspectClauses'; +export * from './RetrieveAccountDetails'; +export * from './RetrieveContractBytecode'; +export * from './RetrieveStoragePositionValue'; +export * from './GetStorageResponse'; diff --git a/packages/net/src/thor/blocks/RegularBlockResponse..ts b/packages/net/src/thor/blocks/RegularBlockResponse..ts new file mode 100644 index 000000000..cfab6b47d --- /dev/null +++ b/packages/net/src/thor/blocks/RegularBlockResponse..ts @@ -0,0 +1,94 @@ +import { Address, ThorId, Units, VTHO } from '@vechain/sdk-core'; +import { UInt } from '../../../../core/src'; + +class RegularBlockResponse { + readonly number: UInt; + readonly id: ThorId; + readonly size: UInt; + readonly parentID: ThorId; + readonly timestamp: bigint; + readonly gasLimit: VTHO; + readonly beneficiary: Address; + readonly gasUsed: VTHO; + readonly totalScore: UInt; + readonly txsRoot: ThorId; + readonly txsFeatures: UInt; + readonly stateRoot: ThorId; + readonly receiptsRoot: ThorId; + readonly com: boolean; + readonly signer: Address; + readonly isTrunk: boolean; + readonly isFinalized: boolean; + readonly transactions: ThorId[]; + + constructor(json: RegularBlockResponseJSON) { + this.number = UInt.of(json.number); + this.id = ThorId.of(json.id); + this.size = UInt.of(json.size); + this.parentID = ThorId.of(json.parentID); + this.timestamp = json.timestamp; + this.gasLimit = VTHO.of(json.gasLimit, Units.wei); + this.beneficiary = Address.of(json.beneficiary); + this.gasUsed = VTHO.of(json.gasUsed, Units.wei); + this.totalScore = UInt.of(json.totalScore); + this.txsRoot = ThorId.of(json.txsRoot); + this.txsFeatures = UInt.of(json.txsFeatures); + this.stateRoot = ThorId.of(json.stateRoot); + this.receiptsRoot = Address.of(json.receiptsRoot); + this.com = json.com; + this.signer = Address.of(json.signer); + this.isTrunk = json.isTrunk; + this.isFinalized = json.isFinalized; + this.transactions = json.transactions.map( + (txId: string): ThorId => ThorId.of(txId) + ); + } + + toJSON(): RegularBlockResponseJSON { + return { + number: this.number.valueOf(), + id: this.id.toString(), + size: this.size.valueOf(), + parentID: this.parentID.toString(), + timestamp: this.timestamp, + gasLimit: Number(this.gasLimit.wei), + beneficiary: this.beneficiary.toString(), + gasUsed: Number(this.gasUsed.wei), + totalScore: this.totalScore.valueOf(), + txsRoot: this.txsRoot.toString(), + txsFeatures: this.txsFeatures.valueOf(), + stateRoot: this.stateRoot.toString(), + receiptsRoot: this.receiptsRoot.toString(), + com: this.com, + signer: this.signer.toString(), + isTrunk: this.isTrunk, + isFinalized: this.isFinalized, + transactions: this.transactions.map((txId: ThorId) => + txId.toString() + ) + } satisfies RegularBlockResponseJSON; + } +} + +interface RegularBlockResponseJSON { + number: number; + id: string; + size: number; + parentID: string; + timestamp: bigint; + gasLimit: number; + beneficiary: string; + gasUsed: number; + totalScore: number; + txsRoot: string; + txsFeatures: number; + stateRoot: string; + receiptsRoot: string; + com: boolean; + signer: string; + isTrunk: boolean; + isFinalized: boolean; + transactions: string[]; +} + +export { RegularBlockResponse, type RegularBlockResponseJSON }; diff --git a/packages/net/src/thor/blocks/RetrieveBlock.ts b/packages/net/src/thor/blocks/RetrieveBlock.ts new file mode 100644 index 000000000..02a30cd2c --- /dev/null +++ b/packages/net/src/thor/blocks/RetrieveBlock.ts @@ -0,0 +1,48 @@ +import { type HttpClient, type HttpPath } from '../../http'; +import { type ThorRequest } from '../ThorRequest'; +import { type ThorResponse } from '../ThorResponse'; +import { + RegularBlockResponse, + type RegularBlockResponseJSON +} from './RegularBlockResponse.'; +import { type Revision } from '@vechain/sdk-core'; + +class RetrieveBlock + implements ThorRequest<RetrieveBlock, RegularBlockResponse> +{ + public readonly path: RetrieveBlockPath; + + constructor(path: RetrieveBlockPath) { + this.path = path; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<RetrieveBlock, RegularBlockResponse>> { + const response = await httpClient.get(this.path, { query: '' }); + const responseBody = + (await response.json()) as RegularBlockResponseJSON; + return { + request: this, + response: new RegularBlockResponse(responseBody) + }; + } + + static of(revision: Revision): RetrieveBlock { + return new RetrieveBlock(new RetrieveBlockPath(revision)); + } +} + +class RetrieveBlockPath implements HttpPath { + readonly revision: Revision; + + constructor(revision: Revision) { + this.revision = revision; + } + + get path(): string { + return `/blocks/${this.revision}`; + } +} + +export { RetrieveBlock, RetrieveBlockPath }; diff --git a/packages/net/src/thor/blocks/index.ts b/packages/net/src/thor/blocks/index.ts new file mode 100644 index 000000000..99bfdd062 --- /dev/null +++ b/packages/net/src/thor/blocks/index.ts @@ -0,0 +1,2 @@ +export * from './RetrieveBlock'; +export * from './RegularBlockResponse.'; diff --git a/packages/net/src/thor/debug/PostDebugTracerCallRequest.ts b/packages/net/src/thor/debug/PostDebugTracerCallRequest.ts new file mode 100644 index 000000000..70663df62 --- /dev/null +++ b/packages/net/src/thor/debug/PostDebugTracerCallRequest.ts @@ -0,0 +1,85 @@ +import { Tracer, type TracerName } from './TracerName'; +import { + Address, + type BlockRef, + HexUInt, + Units, + VET, + VTHO +} from '@vechain/sdk-core'; +import { UInt } from '../../../../core/src'; + +class PostDebugTracerCallRequest { + readonly name?: TracerName; + readonly config?: unknown; + readonly value: VET; + readonly data: HexUInt; + readonly to?: Address; + readonly gas?: VTHO; + readonly gasPrice?: VTHO; + readonly caller?: Address; + readonly provedWork?: string; + readonly gasPayer?: Address; + readonly expiration?: UInt; + readonly blockRef?: BlockRef; + + constructor(json: PostDebugTracerCallRequestJSON) { + this.name = json.name === undefined ? undefined : Tracer.of(json.name); + this.config = json.config; + this.value = VET.of(json.value); + this.data = HexUInt.of(json.data); + this.to = json.to === undefined ? undefined : Address.of(json.to); + this.gas = + json.gas === undefined ? undefined : VTHO.of(json.gas, Units.wei); + this.gasPrice = + json.gasPrice === undefined + ? undefined + : VTHO.of(json.gasPrice, Units.wei); + this.caller = + json.caller === undefined ? undefined : Address.of(json.caller); + this.provedWork = json.provedWork; + this.gasPayer = + json.gasPayer === undefined ? undefined : Address.of(json.gasPayer); + this.expiration = + json.expiration === undefined + ? undefined + : UInt.of(json.expiration); + } + + toJSON(): PostDebugTracerCallRequestJSON { + return { + name: this.name?.toString(), + config: this.config, + value: HexUInt.of(this.value.wei).toString(), + data: this.data.toString(), + to: this.to?.toString(), + gas: this.gas === undefined ? undefined : Number(this.gas.wei), + gasPrice: + this.gasPrice === undefined + ? undefined + : this.gasPrice.wei?.toString(), + caller: this.caller?.toString(), + provedWork: this.provedWork, + gasPayer: this.gasPayer?.toString(), + expiration: this.expiration?.valueOf(), + blockRef: this.blockRef?.toString() + }; + } +} + +interface PostDebugTracerCallRequestJSON { + name?: string; + config?: unknown; + value: string; + data: string; + to?: string; + gas?: number; + gasPrice?: string; + caller?: string; + provedWork?: string; + gasPayer?: string; + expiration?: number; + blockRef?: string; +} + +export { PostDebugTracerCallRequest, type PostDebugTracerCallRequestJSON }; diff --git a/packages/net/src/thor/debug/PostDebugTracerRequest.ts b/packages/net/src/thor/debug/PostDebugTracerRequest.ts new file mode 100644 index 000000000..91bc81484 --- /dev/null +++ b/packages/net/src/thor/debug/PostDebugTracerRequest.ts @@ -0,0 +1,29 @@ +import { Tracer, type TracerName } from './TracerName'; + +class PostDebugTracerRequest { + readonly name?: TracerName; + readonly config?: unknown; + readonly target: string; + + constructor(json: PostDebugTracerRequestJSON) { + this.name = json.name === undefined ? undefined : Tracer.of(json.name); + this.config = json.config; + this.target = json.target; + } + + toJSON(): PostDebugTracerRequestJSON { + return { + name: this.name?.toString(), + config: this.config, + target: this.target + }; + } +} + +interface PostDebugTracerRequestJSON { + name?: string; + config?: unknown; + target: string; +} + +export { PostDebugTracerRequest, type PostDebugTracerRequestJSON }; diff --git a/packages/net/src/thor/debug/RetrieveStorageRange.ts b/packages/net/src/thor/debug/RetrieveStorageRange.ts new file mode 100644 index 000000000..f50827612 --- /dev/null +++ b/packages/net/src/thor/debug/RetrieveStorageRange.ts @@ -0,0 +1,41 @@ +import type { HttpClient, HttpPath } from '../../http'; +import { type ThorRequest } from '../ThorRequest'; +import { + StorageRangeOption, + type StorageRangeOptionJSON +} from './StorageRangeOption'; +import { type ThorResponse } from '../ThorResponse'; +import { type StorageRange } from './StorageRange'; + +class RetrieveStorageRange + implements ThorRequest<RetrieveStorageRange, StorageRange> +{ + static readonly PATH: HttpPath = { path: '/debug/storage-range' }; + + readonly request: StorageRangeOption; + + constructor(request: StorageRangeOption) { + this.request = request; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<RetrieveStorageRange, StorageRange>> { + const response = await httpClient.post( + RetrieveStorageRange.PATH, + { query: '' }, + this.request.toJSON() + ); + const responseBody = (await response.json()) as StorageRange; + return { + request: this, + response: responseBody + }; + } + + static of(request: StorageRangeOptionJSON): RetrieveStorageRange { + return new RetrieveStorageRange(new StorageRangeOption(request)); + } +} + +export { RetrieveStorageRange }; diff --git a/packages/net/src/thor/debug/StorageRange.ts b/packages/net/src/thor/debug/StorageRange.ts new file mode 100644 index 000000000..363d6b799 --- /dev/null +++ b/packages/net/src/thor/debug/StorageRange.ts @@ -0,0 +1,26 @@ +import { ThorId } from '@vechain/sdk-core'; + +class StorageRange { + readonly nextKey?: ThorId; + readonly storage: unknown; + + constructor(json: StorageRangeJSON) { + this.nextKey = + json.nextKey === undefined ? undefined : ThorId.of(json.nextKey); + this.storage = json.storage; + } + + toJSON(): StorageRangeJSON { + return { + nextKey: this.nextKey?.toString(), + storage: this.storage + } satisfies StorageRangeJSON; + } +} + +interface StorageRangeJSON { + nextKey?: string; + storage: unknown; +} + +export { StorageRange, type StorageRangeJSON }; diff --git a/packages/net/src/thor/debug/StorageRangeOption.ts b/packages/net/src/thor/debug/StorageRangeOption.ts new file mode 100644 index 000000000..e521daedf --- /dev/null +++ b/packages/net/src/thor/debug/StorageRangeOption.ts @@ -0,0 +1,36 @@ +import { Address, ThorId } from '@vechain/sdk-core'; +import { UInt } from '../../../../core/src'; + +class StorageRangeOption { + readonly address: Address; + readonly keyStart?: ThorId; + readonly maxResult?: UInt; + readonly target: string; // Path class? + + constructor(json: StorageRangeOptionJSON) { + this.address = Address.of(json.address); + this.keyStart = + json.keyStart === undefined ? undefined : ThorId.of(json.keyStart); + this.maxResult = + json.maxResult === undefined ? undefined : UInt.of(json.maxResult); + this.target = json.target; + } + + toJSON(): StorageRangeOptionJSON { + return { + address: this.address.toString(), + keyStart: this.keyStart?.toString(), + maxResult: this.maxResult?.valueOf(), + target: this.target + } satisfies StorageRangeOptionJSON; + } +} + +interface StorageRangeOptionJSON { + address: string; + keyStart?: string; + maxResult?: number; + target: string; +} + +export { StorageRangeOption, type StorageRangeOptionJSON }; diff --git a/packages/net/src/thor/debug/TraceCall.ts b/packages/net/src/thor/debug/TraceCall.ts new file mode 100644 index 000000000..d74a28023 --- /dev/null +++ b/packages/net/src/thor/debug/TraceCall.ts @@ -0,0 +1,38 @@ +import { type ThorRequest } from '../ThorRequest'; +import { type HttpClient, type HttpPath } from '../../http'; +import { + PostDebugTracerCallRequest, + type PostDebugTracerCallRequestJSON +} from './PostDebugTracerCallRequest'; +import { type ThorResponse } from '../ThorResponse'; + +class TraceCall implements ThorRequest<TraceCall, undefined> { + static readonly PATH: HttpPath = { path: '/debug/tracers/call' }; + + readonly request: PostDebugTracerCallRequest; + + constructor(request: PostDebugTracerCallRequest) { + this.request = request; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<TraceCall, undefined>> { + const response = await httpClient.post( + TraceCall.PATH, + { query: '' }, + this.request.toJSON() + ); + const responseBody: unknown = await response.json(); + return { + request: this, + response: responseBody as undefined + }; + } + + static of(request: PostDebugTracerCallRequestJSON): TraceCall { + return new TraceCall(new PostDebugTracerCallRequest(request)); + } +} + +export { TraceCall }; diff --git a/packages/net/src/thor/debug/TraceTransactionClause.ts b/packages/net/src/thor/debug/TraceTransactionClause.ts new file mode 100644 index 000000000..73e0d6835 --- /dev/null +++ b/packages/net/src/thor/debug/TraceTransactionClause.ts @@ -0,0 +1,40 @@ +import { type ThorRequest } from '../ThorRequest'; +import type { HttpClient, HttpPath } from '../../http'; +import { + PostDebugTracerRequest, + type PostDebugTracerRequestJSON +} from './PostDebugTracerRequest'; +import { type ThorResponse } from '../ThorResponse'; + +class TraceTransactionClause + implements ThorRequest<TraceTransactionClause, unknown> +{ + static readonly PATH: HttpPath = { path: '/debug/tracers' }; + + readonly request: PostDebugTracerRequest; + + constructor(request: PostDebugTracerRequest) { + this.request = request; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<TraceTransactionClause, unknown>> { + const response = await httpClient.post( + TraceTransactionClause.PATH, + { query: '' }, + this.request.toJSON() + ); + const responseBody: unknown = await response.json(); + return { + request: this, + response: responseBody as undefined + }; + } + + static of(request: PostDebugTracerRequestJSON): TraceTransactionClause { + return new TraceTransactionClause(new PostDebugTracerRequest(request)); + } +} + +export { TraceTransactionClause }; diff --git a/packages/net/src/thor/debug/TracerName.ts b/packages/net/src/thor/debug/TracerName.ts new file mode 100644 index 000000000..2372d7ef7 --- /dev/null +++ b/packages/net/src/thor/debug/TracerName.ts @@ -0,0 +1,105 @@ +abstract class TracerName { + abstract toString: () => string; +} + +class StructLogger extends TracerName { + static readonly NAME = 'structLogger'; + toString: () => string = () => StructLogger.NAME; +} + +class FourByte extends TracerName { + static readonly NAME = '4byte'; + toString: () => string = () => FourByte.NAME; +} + +class Call extends TracerName { + static readonly NAME = 'call'; + toString: () => string = () => Call.NAME; +} + +class Noop extends TracerName { + static readonly NAME = 'noop'; + toString: () => string = () => Noop.NAME; +} + +class Prestate extends TracerName { + static readonly NAME = 'prestate'; + toString: () => string = () => Prestate.NAME; +} + +class Unigram extends TracerName { + static readonly NAME = 'unigram'; + toString: () => string = () => Unigram.NAME; +} + +class Bigram extends TracerName { + static readonly NAME = 'bigram'; + toString: () => string = () => Bigram.NAME; +} + +class Trigram extends TracerName { + static readonly NAME = 'trigram'; + toString: () => string = () => Trigram.NAME; +} + +class EvmDis extends TracerName { + static readonly NAME = 'evmdis'; + toString: () => string = () => EvmDis.NAME; +} + +class OpCount extends TracerName { + static readonly NAME = 'opcount'; + toString: () => string = () => OpCount.NAME; +} + +class Null extends TracerName { + static readonly NAME = 'null'; + toString: () => string = () => Null.NAME; +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +class Tracer { + static of(name: string): TracerName { + switch (name) { + case StructLogger.NAME: + return new StructLogger(); + case FourByte.NAME: + return new FourByte(); + case Call.NAME: + return new Call(); + case Noop.NAME: + return new Noop(); + case Prestate.NAME: + return new Prestate(); + case Unigram.NAME: + return new Unigram(); + case Bigram.NAME: + return new Bigram(); + case Trigram.NAME: + return new Trigram(); + case EvmDis.NAME: + return new EvmDis(); + case OpCount.NAME: + return new OpCount(); + case Null.NAME: + return new Null(); + } + throw new Error(`TracerName ${name} not found`); + } +} + +export { + type TracerName, + StructLogger, + FourByte, + Call, + Noop, + Prestate, + Unigram, + Bigram, + Trigram, + EvmDis, + OpCount, + Null, + Tracer +}; diff --git a/packages/net/src/thor/debug/index.ts b/packages/net/src/thor/debug/index.ts new file mode 100644 index 000000000..0a03ce0a4 --- /dev/null +++ b/packages/net/src/thor/debug/index.ts @@ -0,0 +1,8 @@ +export * from './PostDebugTracerCallRequest'; +export * from './PostDebugTracerRequest'; +export * from './RetrieveStorageRange'; +export * from './StorageRange'; +export * from './StorageRangeOption'; +export * from './TraceCall'; +export * from './TracerName'; +export * from './TraceTransactionClause'; diff --git a/packages/net/src/thor/explorer/index.ts b/packages/net/src/thor/explorer/index.ts new file mode 100644 index 000000000..21fb5819e --- /dev/null +++ b/packages/net/src/thor/explorer/index.ts @@ -0,0 +1,42 @@ +import { FetchHttpClient } from '../../http'; +import { ThorNetworks } from '../ThorNetworks'; +import { RetrieveBlock, RetrieveBlockPath } from '../blocks'; +import { Revision } from '@vechain/sdk-core'; +import { RetrieveTransactionByID } from '../transactions'; + +import { TxId } from '../../../../core/src/vcdm/BlockId'; + +async function getBestBlockNumber( + httpClient: FetchHttpClient +): Promise<number> { + const r = await new RetrieveBlock( + new RetrieveBlockPath(Revision.BEST) + ).askTo(httpClient); + return r.response.number.valueOf(); +} + +async function explore(): Promise<void> { + const httpClient = FetchHttpClient.at(ThorNetworks.SOLONET); + const lastBlockNumber = await getBestBlockNumber(httpClient); + for (let blockNumber = lastBlockNumber; blockNumber >= 0; blockNumber--) { + const block = ( + await RetrieveBlock.of(Revision.of(blockNumber)).askTo(httpClient) + ).response; + console.log(`BLOCK ${block.number} of ${lastBlockNumber}`); + for (const txid of block.transactions) { + console.log(`TXID ${txid}`); + const tx = ( + await RetrieveTransactionByID.of( + TxId.of(txid.toString()) + ).askTo(httpClient) + ).response; + + console.log(`TX: ${JSON.stringify(tx, null, 2)}`); + } + } +} + +console.log('Thor Scanner...'); +void explore().then((_r) => { + console.log('End of all jobs.'); +}); diff --git a/packages/net/src/thor/index.ts b/packages/net/src/thor/index.ts new file mode 100644 index 000000000..846caa20b --- /dev/null +++ b/packages/net/src/thor/index.ts @@ -0,0 +1,7 @@ +export * from './accounts'; +export * from './node'; +export * from './blocks'; +export * from './ThorNetworks'; +export * from './ThorRequest'; +export * from './ThorResponse'; +export * from './transactions'; diff --git a/packages/net/src/thor/logs/EventCriteria.ts b/packages/net/src/thor/logs/EventCriteria.ts new file mode 100644 index 000000000..9831188de --- /dev/null +++ b/packages/net/src/thor/logs/EventCriteria.ts @@ -0,0 +1,47 @@ +import { Address, ThorId } from '@vechain/sdk-core'; + +class EventCriteria { + readonly address?: Address; + readonly topic0?: ThorId; + readonly topic1?: ThorId; + readonly topic2?: ThorId; + readonly topic3?: ThorId; + readonly topic4?: ThorId; + + constructor(json: EventCriteriaJSON) { + this.address = + json.address === undefined ? undefined : Address.of(json.address); + this.topic0 = + json.topic0 === undefined ? undefined : ThorId.of(json.topic0); + this.topic1 = + json.topic1 === undefined ? undefined : ThorId.of(json.topic1); + this.topic2 = + json.topic2 === undefined ? undefined : ThorId.of(json.topic2); + this.topic3 = + json.topic3 === undefined ? undefined : ThorId.of(json.topic3); + this.topic4 = + json.topic4 === undefined ? undefined : ThorId.of(json.topic4); + } + + toJSON(): EventCriteriaJSON { + return { + address: this.address?.toString(), + topic0: this.topic0?.toString(), + topic1: this.topic1?.toString(), + topic2: this.topic2?.toString(), + topic3: this.topic3?.toString(), + topic4: this.topic4?.toString() + } satisfies EventCriteriaJSON; + } +} + +interface EventCriteriaJSON { + address?: string; + topic0?: string; + topic1?: string; + topic2?: string; + topic3?: string; + topic4?: string; +} + +export { EventCriteria, type EventCriteriaJSON }; diff --git a/packages/net/src/thor/logs/EventLogFilterRequest.ts b/packages/net/src/thor/logs/EventLogFilterRequest.ts new file mode 100644 index 000000000..197ee1ac8 --- /dev/null +++ b/packages/net/src/thor/logs/EventLogFilterRequest.ts @@ -0,0 +1,46 @@ +import { FilterRange, type FilterRangeJSON } from './FilterRange'; +import { EventCriteria, type EventCriteriaJSON } from './EventCriteria'; +import { FilterOptions, type FilterOptionsJSON } from './FilterOptions'; +import { type LogSort } from './LogSort'; + +class EventLogFilterRequest { + readonly range?: FilterRange; + readonly options?: FilterOptions; + readonly criteriaSet?: EventCriteria[]; + readonly order?: LogSort; + + constructor(json: EventLogFilterRequestJSON) { + this.range = + json.range === undefined ? undefined : new FilterRange(json.range); + this.options = + json.options === undefined + ? undefined + : new FilterOptions(json.options); + this.criteriaSet = + json.criteriaSet === undefined + ? undefined + : json.criteriaSet.map( + (criteriaJSON) => new EventCriteria(criteriaJSON) + ); + this.order = + json.order === undefined ? undefined : (json.order as LogSort); + } + + toJSON(): EventLogFilterRequestJSON { + return { + range: this.range?.toJSON(), + options: this.options?.toJSON(), + criteriaSet: this.criteriaSet?.map((criteria) => criteria.toJSON()), + order: this.order?.toString() + } satisfies EventLogFilterRequestJSON; + } +} + +interface EventLogFilterRequestJSON { + range?: FilterRangeJSON; + options?: FilterOptionsJSON; + criteriaSet?: EventCriteriaJSON[]; + order?: string; +} + +export { EventLogFilterRequest, type EventLogFilterRequestJSON }; diff --git a/packages/net/src/thor/logs/EventLogsResponse.ts b/packages/net/src/thor/logs/EventLogsResponse.ts new file mode 100644 index 000000000..1ce266b29 --- /dev/null +++ b/packages/net/src/thor/logs/EventLogsResponse.ts @@ -0,0 +1,56 @@ +import { LogMeta, type LogMetaJSON } from './LogMeta'; +import { Address, HexUInt, ThorId } from '@vechain/sdk-core'; + +class EventLogResponse { + readonly address: Address; + readonly topics: ThorId[]; + readonly data: HexUInt; + readonly meta: LogMeta; + + constructor(json: EventLogResponseJSON) { + this.address = Address.of(json.address); + this.topics = json.topics.map( + (topic: string): ThorId => ThorId.of(topic) + ); + this.data = HexUInt.of(json.data); + this.meta = new LogMeta(json.meta); + } + + toJSON(): EventLogResponseJSON { + return { + address: this.address.toString(), + topics: this.topics.map((topic: ThorId): string => + topic.toString() + ), + data: this.data.toString(), + meta: this.meta.toJSON() + } satisfies EventLogResponseJSON; + } +} + +interface EventLogResponseJSON { + address: string; + topics: string[]; + data: string; + meta: LogMetaJSON; +} + +class EventLogsResponse extends Array<EventLogResponse> { + constructor(json: EventLogsResponseJSON) { + super( + ...json.map( + (response: EventLogResponseJSON): EventLogResponse => + new EventLogResponse(response) + ) + ); + } +} + +interface EventLogsResponseJSON extends Array<EventLogResponseJSON> {} + +export { + EventLogResponse, + type EventLogResponseJSON, + type EventLogsResponseJSON, + EventLogsResponse +}; diff --git a/packages/net/src/thor/logs/FilterOptions.ts b/packages/net/src/thor/logs/FilterOptions.ts new file mode 100644 index 000000000..54c212af6 --- /dev/null +++ b/packages/net/src/thor/logs/FilterOptions.ts @@ -0,0 +1,26 @@ +import { UInt } from '../../../../core/src'; + +class FilterOptions { + readonly limit?: UInt; + readonly offset?: UInt; + + constructor(json: FilterOptionsJSON) { + this.limit = json.limit === undefined ? undefined : UInt.of(json.limit); + this.offset = + json.offset === undefined ? undefined : UInt.of(json.offset); + } + + toJSON(): FilterOptionsJSON { + return { + limit: this.limit?.valueOf(), + offset: this.offset?.valueOf() + } satisfies FilterOptionsJSON; + } +} + +interface FilterOptionsJSON { + limit?: number; + offset?: number; +} + +export { FilterOptions, type FilterOptionsJSON }; diff --git a/packages/net/src/thor/logs/FilterRange.ts b/packages/net/src/thor/logs/FilterRange.ts new file mode 100644 index 000000000..658df799e --- /dev/null +++ b/packages/net/src/thor/logs/FilterRange.ts @@ -0,0 +1,38 @@ +import { UInt } from '../../../../core/src'; + +class FilterRange { + readonly unit?: FilterRangeUnits; + readonly from?: UInt; + readonly to?: UInt; + + constructor(json: FilterRangeJSON) { + this.unit = + typeof json.unit === 'string' + ? (json.unit as FilterRangeUnits) + : undefined; + this.from = + typeof json.from === 'number' ? UInt.of(json.from) : undefined; + this.to = typeof json.to === 'number' ? UInt.of(json.to) : undefined; + } + + toJSON(): FilterRangeJSON { + return { + unit: this.unit?.toString(), + from: this.from?.valueOf(), + to: this.to?.valueOf() + } satisfies FilterRangeJSON; + } +} + +interface FilterRangeJSON { + unit?: string; + from?: number; + to?: number; +} + +enum FilterRangeUnits { + block = 'block', + time = 'time' +} + +export { FilterRange, type FilterRangeJSON, FilterRangeUnits }; diff --git a/packages/net/src/thor/logs/LogMeta.ts b/packages/net/src/thor/logs/LogMeta.ts new file mode 100644 index 000000000..7a2b3b61b --- /dev/null +++ b/packages/net/src/thor/logs/LogMeta.ts @@ -0,0 +1,42 @@ +import { Address, BlockId } from '@vechain/sdk-core'; +import { TxId, UInt } from '../../../../core/src/vcdm'; + +class LogMeta { + readonly blockID: BlockId; + readonly blockNumber: UInt; + readonly blockTimestamp: UInt; + readonly txID: TxId; + readonly txOrigin: Address; + readonly clauseIndex: UInt; + + constructor(json: LogMetaJSON) { + this.blockID = BlockId.of(json.blockID); + this.blockNumber = UInt.of(json.blockNumber); + this.blockTimestamp = UInt.of(json.blockTimestamp); + this.txID = TxId.of(json.txID); + this.txOrigin = Address.of(json.txOrigin); + this.clauseIndex = UInt.of(json.clauseIndex); + } + + toJSON(): LogMetaJSON { + return { + blockID: this.blockID.toString(), + blockNumber: this.blockNumber.valueOf(), + blockTimestamp: this.blockTimestamp.valueOf(), + txID: this.txID.toString(), + txOrigin: this.txOrigin.toString(), + clauseIndex: this.clauseIndex.valueOf() + } satisfies LogMetaJSON; + } +} + +interface LogMetaJSON { + blockID: string; + blockNumber: number; + blockTimestamp: number; + txID: string; + txOrigin: string; + clauseIndex: number; +} + +export { LogMeta, type LogMetaJSON }; diff --git a/packages/net/src/thor/logs/LogSort.ts b/packages/net/src/thor/logs/LogSort.ts new file mode 100644 index 000000000..d31aec9de --- /dev/null +++ b/packages/net/src/thor/logs/LogSort.ts @@ -0,0 +1,6 @@ +enum LogSort { + asc = 'asc', + desc = 'desc' +} + +export { LogSort }; diff --git a/packages/net/src/thor/logs/QuerySmartContractEvents.ts b/packages/net/src/thor/logs/QuerySmartContractEvents.ts new file mode 100644 index 000000000..097298b51 --- /dev/null +++ b/packages/net/src/thor/logs/QuerySmartContractEvents.ts @@ -0,0 +1,44 @@ +import { type ThorRequest } from '../ThorRequest'; +import type { HttpClient, HttpPath } from '../../http'; +import { + EventLogFilterRequest, + type EventLogFilterRequestJSON +} from './EventLogFilterRequest'; +import type { ThorResponse } from '../ThorResponse'; +import { + EventLogsResponse, + type EventLogsResponseJSON +} from './EventLogsResponse'; + +class QuerySmartContractEvents + implements ThorRequest<QuerySmartContractEvents, EventLogsResponse> +{ + static readonly PATH: HttpPath = { path: '/logs/event' }; + + readonly request: EventLogFilterRequest; + + constructor(request: EventLogFilterRequest) { + this.request = request; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<QuerySmartContractEvents, EventLogsResponse>> { + const response = await httpClient.post( + QuerySmartContractEvents.PATH, + { query: '' }, + this.request.toJSON() + ); + const responseBody = (await response.json()) as EventLogsResponseJSON; + return { + request: this, + response: new EventLogsResponse(responseBody) + }; + } + + static of(request: EventLogFilterRequestJSON): QuerySmartContractEvents { + return new QuerySmartContractEvents(new EventLogFilterRequest(request)); + } +} + +export { QuerySmartContractEvents }; diff --git a/packages/net/src/thor/logs/QueryVETTransferEvents.ts b/packages/net/src/thor/logs/QueryVETTransferEvents.ts new file mode 100644 index 000000000..4296b3e59 --- /dev/null +++ b/packages/net/src/thor/logs/QueryVETTransferEvents.ts @@ -0,0 +1,52 @@ +import { + TransferLogResponse, + type TransferLogResponseJSON, + type TransferLogsResponse, + type TransferLogsResponseJSON +} from './TransferLogsResponse'; +import { type ThorRequest } from '../ThorRequest'; +import type { HttpClient, HttpPath } from '../../http'; +import { + TransferLogFilterRequest, + type TransferLogFilterRequestJSON +} from './TransferLogFilterRequest'; +import { type ThorResponse } from '../ThorResponse'; + +class QueryVETTransferEvents + implements ThorRequest<QueryVETTransferEvents, TransferLogsResponse> +{ + static readonly PATH: HttpPath = { path: '/logs/transfer' }; + + readonly request: TransferLogFilterRequest; + + constructor(request: TransferLogFilterRequest) { + this.request = request; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<QueryVETTransferEvents, TransferLogsResponse>> { + const response = await httpClient.post( + QueryVETTransferEvents.PATH, + { query: '' }, + this.request.toJSON() + ); + const responseBody = + (await response.json()) as TransferLogsResponseJSON; + return { + request: this, + response: responseBody.map( + (json: TransferLogResponseJSON): TransferLogResponse => + new TransferLogResponse(json) + ) as TransferLogsResponse + }; + } + + static of(request: TransferLogFilterRequestJSON): QueryVETTransferEvents { + return new QueryVETTransferEvents( + new TransferLogFilterRequest(request) + ); + } +} + +export { QueryVETTransferEvents }; diff --git a/packages/net/src/thor/logs/TransferCriteria.ts b/packages/net/src/thor/logs/TransferCriteria.ts new file mode 100644 index 000000000..e7eadbb8c --- /dev/null +++ b/packages/net/src/thor/logs/TransferCriteria.ts @@ -0,0 +1,34 @@ +import { Address } from '@vechain/sdk-core'; + +class TransferCriteria { + readonly txOrigin?: Address; + readonly sender?: Address; + readonly recipient?: Address; + + constructor(json: TransferCriteriaJSON) { + this.txOrigin = + json.txOrigin === undefined ? undefined : Address.of(json.txOrigin); + this.sender = + json.sender === undefined ? undefined : Address.of(json.sender); + this.recipient = + json.recipient === undefined + ? undefined + : Address.of(json.recipient); + } + + toJSON(): TransferCriteriaJSON { + return { + txOrigin: this.txOrigin?.toString(), + sender: this.sender?.toString(), + recipient: this.recipient?.toString() + } satisfies TransferCriteriaJSON; + } +} + +interface TransferCriteriaJSON { + txOrigin?: string; + sender?: string; + recipient?: string; +} + +export { TransferCriteria, type TransferCriteriaJSON }; diff --git a/packages/net/src/thor/logs/TransferLogFilterRequest.ts b/packages/net/src/thor/logs/TransferLogFilterRequest.ts new file mode 100644 index 000000000..8257c903f --- /dev/null +++ b/packages/net/src/thor/logs/TransferLogFilterRequest.ts @@ -0,0 +1,46 @@ +import { FilterRange, type FilterRangeJSON } from './FilterRange'; +import { FilterOptions, type FilterOptionsJSON } from './FilterOptions'; +import { type LogSort } from './LogSort'; +import { + TransferCriteria, + type TransferCriteriaJSON +} from './TransferCriteria'; + +class TransferLogFilterRequest { + readonly range?: FilterRange; + readonly options?: FilterOptions; + readonly criteriaSet?: TransferCriteria[]; + readonly order?: LogSort; + + constructor(json: TransferLogFilterRequestJSON) { + this.range = + json.range === undefined ? undefined : new FilterRange(json.range); + this.options = + json.options === undefined + ? undefined + : new FilterOptions(json.options); + this.criteriaSet = json.criteriaSet?.map( + (criteriaJSON) => new TransferCriteria(criteriaJSON) + ); + this.order = + json.order === undefined ? undefined : (json.order as LogSort); + } + + toJSON(): TransferLogFilterRequestJSON { + return { + range: this.range?.toJSON(), + options: this.options?.toJSON(), + criteriaSet: this.criteriaSet?.map((criteria) => criteria.toJSON()), + order: this.order + } satisfies TransferLogFilterRequestJSON; + } +} + +interface TransferLogFilterRequestJSON { + range?: FilterRangeJSON; + options?: FilterOptionsJSON; + criteriaSet?: TransferCriteriaJSON[]; + order?: string; +} + +export { TransferLogFilterRequest, type TransferLogFilterRequestJSON }; diff --git a/packages/net/src/thor/logs/TransferLogsResponse.ts b/packages/net/src/thor/logs/TransferLogsResponse.ts new file mode 100644 index 000000000..c0b597e8d --- /dev/null +++ b/packages/net/src/thor/logs/TransferLogsResponse.ts @@ -0,0 +1,43 @@ +import { LogMeta, type LogMetaJSON } from './LogMeta'; +import { Address, HexUInt, Units, VET } from '@vechain/sdk-core'; + +class TransferLogResponse { + readonly sender: Address; + readonly recipient: Address; + readonly amount: VET; + readonly meta: LogMeta; + + constructor(json: TransferLogResponseJSON) { + this.sender = Address.of(json.sender); + this.recipient = Address.of(json.recipient); + this.amount = VET.of(HexUInt.of(json.amount).bi, Units.wei); + this.meta = new LogMeta(json.meta); + } + + toJSON(): TransferLogResponseJSON { + return { + sender: this.sender.toString(), + recipient: this.recipient.toString(), + amount: HexUInt.of(this.amount.wei).toString(), + meta: this.meta.toJSON() + } satisfies TransferLogResponseJSON; + } +} + +interface TransferLogResponseJSON { + sender: string; + recipient: string; + amount: string; + meta: LogMetaJSON; +} + +interface TransferLogsResponse extends Array<TransferLogResponse> {} + +interface TransferLogsResponseJSON extends Array<TransferLogResponseJSON> {} + +export { + TransferLogResponse, + type TransferLogResponseJSON, + type TransferLogsResponse, + type TransferLogsResponseJSON +}; diff --git a/packages/net/src/thor/logs/index.ts b/packages/net/src/thor/logs/index.ts new file mode 100644 index 000000000..2a810e494 --- /dev/null +++ b/packages/net/src/thor/logs/index.ts @@ -0,0 +1,10 @@ +export * from './EventCriteria'; +export * from './EventLogFilterRequest'; +export * from './EventLogsResponse'; +export * from './FilterOptions'; +export * from './FilterRange'; +export * from './LogMeta'; +export * from './LogSort'; +export * from './QuerySmartContractEvents'; +export * from './TransferLogsResponse'; +export * from './TransferCriteria'; diff --git a/packages/net/src/thor/node/GetPeersResponse.ts b/packages/net/src/thor/node/GetPeersResponse.ts new file mode 100644 index 000000000..90fd3b3f5 --- /dev/null +++ b/packages/net/src/thor/node/GetPeersResponse.ts @@ -0,0 +1,11 @@ +import { PeerStat, type PeerStatJSON } from './PeerStat'; + +class GetPeersResponse extends Array<PeerStat> { + constructor(json: GetPeersResponseJSON) { + super(...json.map((json: PeerStatJSON) => new PeerStat(json))); + } +} + +interface GetPeersResponseJSON extends Array<PeerStatJSON> {} + +export { GetPeersResponse, type GetPeersResponseJSON }; diff --git a/packages/net/src/thor/node/PeerStat.ts b/packages/net/src/thor/node/PeerStat.ts new file mode 100644 index 000000000..35e187716 --- /dev/null +++ b/packages/net/src/thor/node/PeerStat.ts @@ -0,0 +1,47 @@ +import { BlockId } from '@vechain/sdk-core'; + +import { UInt } from '../../../../core/src/vcdm/UInt'; + +class PeerStat { + readonly name: string; + readonly bestBlockID: BlockId; + readonly totalScore: UInt; + readonly peerID: string; + readonly netAddr: string; + readonly inbound: boolean; + readonly duration: UInt; + + constructor(json: PeerStatJSON) { + this.name = json.name; + this.bestBlockID = BlockId.of(json.bestBlockID); + this.totalScore = UInt.of(json.totalScore); + this.peerID = json.peerID; + this.netAddr = json.netAddr; + this.inbound = json.inbound; + this.duration = UInt.of(json.duration); + } + + toJSON(): PeerStatJSON { + return { + name: this.name, + bestBlockID: this.bestBlockID.toString(), + totalScore: this.totalScore.valueOf(), + peerID: this.peerID, + netAddr: this.netAddr, + inbound: this.inbound, + duration: this.duration.valueOf() + }; + } +} + +interface PeerStatJSON { + name: string; + bestBlockID: string; + totalScore: number; + peerID: string; + netAddr: string; + inbound: boolean; + duration: number; +} + +export { PeerStat, type PeerStatJSON }; diff --git a/packages/net/src/thor/node/RetrieveConnectedPeers.ts b/packages/net/src/thor/node/RetrieveConnectedPeers.ts new file mode 100644 index 000000000..5a07dde0e --- /dev/null +++ b/packages/net/src/thor/node/RetrieveConnectedPeers.ts @@ -0,0 +1,29 @@ +import { + GetPeersResponse, + type GetPeersResponseJSON +} from './GetPeersResponse'; +import { type HttpClient, type HttpPath } from '../../http'; +import { type ThorRequest } from '../ThorRequest'; +import { type ThorResponse } from '../ThorResponse'; + +class RetrieveConnectedPeers + implements ThorRequest<RetrieveConnectedPeers, GetPeersResponse> +{ + private static readonly PATH: HttpPath = { path: '/node/network/peers' }; + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<RetrieveConnectedPeers, GetPeersResponse>> { + const response = await httpClient.get(RetrieveConnectedPeers.PATH, { + query: '' + }); + const responseBody: GetPeersResponseJSON = + (await response.json()) as GetPeersResponseJSON; + return { + request: this, + response: new GetPeersResponse(responseBody) + }; + } +} + +export { RetrieveConnectedPeers }; diff --git a/packages/net/src/thor/node/index.ts b/packages/net/src/thor/node/index.ts new file mode 100644 index 000000000..95e495133 --- /dev/null +++ b/packages/net/src/thor/node/index.ts @@ -0,0 +1,3 @@ +export * from './GetPeersResponse'; +export * from './PeerStat'; +export * from './RetrieveConnectedPeers'; diff --git a/packages/net/src/thor/subscriptions/BeatsSubscription.ts b/packages/net/src/thor/subscriptions/BeatsSubscription.ts new file mode 100644 index 000000000..c9af00f69 --- /dev/null +++ b/packages/net/src/thor/subscriptions/BeatsSubscription.ts @@ -0,0 +1,101 @@ +import { type WebSocketClient, type WebSocketListener } from '../../ws'; +import type { HttpPath } from '../../http'; +import type { BlockId } from '@vechain/sdk-core'; +import { + SubscriptionBeat2Response, + type SubscriptionBeat2ResponseJSON +} from './SubscriptionBeat2Response'; + +class BeatsSubscription implements WebSocketClient, WebSocketListener<unknown> { + static readonly PATH: HttpPath = { path: '/subscriptions/beat2' }; + + private readonly listeners: Array< + WebSocketListener<SubscriptionBeat2Response> + > = []; + + private readonly query: BeatsSubscriptionQuery; + + private readonly wsc: WebSocketClient; + + protected constructor(wsc: WebSocketClient, query: BeatsSubscriptionQuery) { + this.wsc = wsc; + this.query = query; + } + + addListener(listener: WebSocketListener<SubscriptionBeat2Response>): this { + this.listeners.push(listener); + return this; + } + + static at(wsc: WebSocketClient): BeatsSubscription { + return new BeatsSubscription(wsc, new BeatsSubscriptionQuery()); + } + + get baseURL(): string { + return this.wsc.baseURL; + } + + close(): this { + this.wsc.close(); + return this; + } + + onClose(event: Event): void { + this.listeners.forEach((listener) => { + listener.onClose(event); + }); + } + + onError(event: Event): void { + this.listeners.forEach((listener) => { + listener.onError(event); + }); + } + + onMessage(event: MessageEvent<unknown>): void { + const json = JSON.parse( + event.data as string + ) as SubscriptionBeat2ResponseJSON; + const message = new MessageEvent<SubscriptionBeat2Response>( + event.type, + { data: new SubscriptionBeat2Response(json) } + ); + this.listeners.forEach((listener) => { + listener.onMessage(message); + }); + } + + onOpen(event: Event): void { + this.listeners.forEach((listener) => { + listener.onOpen(event); + }); + } + + open(): this { + this.wsc + .addListener(this) + .open({ path: BeatsSubscription.PATH.path + this.query.query }); + return this; + } + + removeListener( + listener: WebSocketListener<SubscriptionBeat2Response> + ): this { + this.listeners.splice(this.listeners.indexOf(listener), 1); + return this; + } +} + +class BeatsSubscriptionQuery { + readonly pos?: BlockId; + + constructor(pos?: BlockId) { + this.pos = pos; + } + + get query(): string { + return this.pos === undefined ? '' : `?pos=${this.pos}`; + } +} + +export { BeatsSubscription }; diff --git a/packages/net/src/thor/subscriptions/BlocksSubscription.ts b/packages/net/src/thor/subscriptions/BlocksSubscription.ts new file mode 100644 index 000000000..5ee594aa6 --- /dev/null +++ b/packages/net/src/thor/subscriptions/BlocksSubscription.ts @@ -0,0 +1,110 @@ +import { type HttpPath, type HttpQuery } from '../../http'; +import { type WebSocketClient, type WebSocketListener } from '../../ws'; +import { + SubscriptionBlockResponse, + type SubscriptionBlockResponseJSON +} from './SubscriptionBlockResponse'; +import { type BlockId } from '@vechain/sdk-core'; + +class BlocksSubscription + implements WebSocketClient, WebSocketListener<unknown> +{ + static readonly PATH: HttpPath = { path: '/subscriptions/block' }; + + private readonly listeners: Array< + WebSocketListener<SubscriptionBlockResponse> + > = []; + + private readonly query: BlockSubscriptionQuery; + + private readonly wsc: WebSocketClient; + + protected constructor(wsc: WebSocketClient, query: BlockSubscriptionQuery) { + this.wsc = wsc; + this.query = query; + } + + addListener(listener: WebSocketListener<SubscriptionBlockResponse>): this { + this.listeners.push(listener); + return this; + } + + static at(wsc: WebSocketClient): BlocksSubscription { + return new BlocksSubscription(wsc, new BlockSubscriptionQuery()); + } + + atPos(pos?: BlockId): BlocksSubscription { + return new BlocksSubscription( + this.wsc, + new BlockSubscriptionQuery(pos ?? this.query.pos) + ); + } + + get baseURL(): string { + return this.wsc.baseURL; + } + + close(): this { + this.wsc.close(); + return this; + } + + onClose(event: Event): void { + this.listeners.forEach((listener) => { + listener.onClose(event); + }); + } + + onError(event: Event): void { + this.listeners.forEach((listener) => { + listener.onError(event); + }); + } + + onMessage(event: MessageEvent<unknown>): void { + const json = JSON.parse( + event.data as string + ) as SubscriptionBlockResponseJSON; + const message = new MessageEvent<SubscriptionBlockResponse>( + event.type, + { data: new SubscriptionBlockResponse(json) } + ); + this.listeners.forEach((listener) => { + listener.onMessage(message); + }); + } + + onOpen(event: Event): void { + this.listeners.forEach((listener) => { + listener.onOpen(event); + }); + } + + open(): this { + this.wsc + .addListener(this) + .open({ path: BlocksSubscription.PATH.path + this.query.query }); + return this; + } + + removeListener( + listener: WebSocketListener<SubscriptionBlockResponse> + ): this { + this.listeners.splice(this.listeners.indexOf(listener), 1); + return this; + } +} + +class BlockSubscriptionQuery implements HttpQuery { + readonly pos?: BlockId; + + constructor(pos?: BlockId) { + this.pos = pos; + } + + get query(): string { + return this.pos === undefined ? '' : `?pos=${this.pos}`; + } +} + +export { BlocksSubscription }; diff --git a/packages/net/src/thor/subscriptions/EventsSubscription.ts b/packages/net/src/thor/subscriptions/EventsSubscription.ts new file mode 100644 index 000000000..4e0ff9af9 --- /dev/null +++ b/packages/net/src/thor/subscriptions/EventsSubscription.ts @@ -0,0 +1,191 @@ +import { type WebSocketClient, type WebSocketListener } from '../../ws'; +import { type SubscriptionEventResponse } from './SubscriptionEventResponse'; +import { type HttpPath, type HttpQuery } from '../../http'; +import { type Address, type ThorId } from '@vechain/sdk-core'; + +class EventsSubscription + implements WebSocketClient, WebSocketListener<SubscriptionEventResponse> +{ + static readonly PATH: HttpPath = { path: '/subscriptions/event' }; + + private readonly listeners: Array< + WebSocketListener<SubscriptionEventResponse> + > = []; + + private readonly query: EventsSubscriptionQuery; + + private readonly wsc: WebSocketClient; + + protected constructor( + wsc: WebSocketClient, + query: EventsSubscriptionQuery + ) { + this.wsc = wsc; + this.query = query; + } + + addListener(listener: WebSocketListener<SubscriptionEventResponse>): this { + this.listeners.push(listener); + return this; + } + + static at(wsc: WebSocketClient): EventsSubscription { + return new EventsSubscription(wsc, new EventsSubscriptionQuery()); + } + + atPos(pos?: ThorId): EventsSubscription { + return new EventsSubscription( + this.wsc, + new EventsSubscriptionQuery( + this.query.addr, + pos ?? this.query.pos, + this.query.t0, + this.query.t1, + this.query.t2, + this.query.t3 + ) + ); + } + + get baseURL(): string { + return this.wsc.baseURL; + } + + close(): this { + this.wsc.close(); + return this; + } + + onClose(event: Event): void { + this.listeners.forEach((listener) => { + listener.onClose(event); + }); + } + + onError(event: Event): void { + this.listeners.forEach((listener) => { + listener.onError(event); + }); + } + + onMessage(event: MessageEvent<unknown>): void { + const json = JSON.parse( + event.data as string + ) as SubscriptionEventResponse; + const message = new MessageEvent<SubscriptionEventResponse>( + event.type, + { data: json } + ); + this.listeners.forEach((listener) => { + listener.onMessage(message); + }); + } + + onOpen(event: Event): void { + this.listeners.forEach((listener) => { + listener.onOpen(event); + }); + } + + open(): this { + this.wsc + .addListener(this) + .open({ path: EventsSubscription.PATH.path + this.query.query }); + return this; + } + + removeListener( + listener: WebSocketListener<SubscriptionEventResponse> + ): this { + this.listeners.splice(this.listeners.indexOf(listener), 1); + return this; + } + + withContractAddress(contractAddress?: Address): EventsSubscription { + return new EventsSubscription( + this.wsc, + new EventsSubscriptionQuery( + contractAddress ?? this.query.addr, + this.query.pos, + this.query.t0, + this.query.t1, + this.query.t2, + this.query.t3 + ) + ); + } + + withFilters( + t0?: ThorId, + t1?: ThorId, + t2?: ThorId, + t3?: ThorId + ): EventsSubscription { + return new EventsSubscription( + this.wsc, + new EventsSubscriptionQuery( + this.query.addr, + this.query.pos, + t0 ?? this.query.t0, + t1 ?? this.query.t1, + t2 ?? this.query.t2, + t3 ?? this.query.t3 + ) + ); + } +} + +class EventsSubscriptionQuery implements HttpQuery { + readonly addr?: Address; + readonly pos?: ThorId; + readonly t0?: ThorId; + readonly t1?: ThorId; + readonly t2?: ThorId; + readonly t3?: ThorId; + + constructor( + addr?: Address, + pos?: ThorId, + t0?: ThorId, + t1?: ThorId, + t2?: ThorId, + t3?: ThorId + ) { + this.addr = addr; + this.pos = pos; + this.t0 = t0; + this.t1 = t1; + this.t2 = t2; + this.t3 = t3; + } + + get query(): string { + let query = ''; + if (this.addr !== undefined) { + query += `addr=${this.addr}`; + } + if (this.pos !== undefined) { + if (query.length > 0) query += '&'; + query += `pos=${this.pos}`; + } + if (this.t0 !== undefined) { + if (query.length > 0) query += '&'; + query += `t0=${this.t0}`; + } + if (this.t1 !== undefined) { + if (query.length > 0) query += '&'; + query += `t1=${this.t1}`; + } + if (this.t2 !== undefined) { + if (query.length > 0) query += '&'; + query += `t2=${this.t2}`; + } + if (this.t3 !== undefined) { + if (query.length > 0) query += '&'; + query += `t3=${this.t3}`; + } + return query.length > 0 ? `?${query}` : query; + } +} + +export { EventsSubscription }; diff --git a/packages/net/src/thor/subscriptions/NewTransactionSubscription.ts b/packages/net/src/thor/subscriptions/NewTransactionSubscription.ts new file mode 100644 index 000000000..51a58f203 --- /dev/null +++ b/packages/net/src/thor/subscriptions/NewTransactionSubscription.ts @@ -0,0 +1,75 @@ +import { type WebSocketClient, type WebSocketListener } from '../../ws'; +import type { HttpPath } from '../../http'; +import { TXID, type TXIDJSON } from '../transactions/TXID'; + +class NewTransactionSubscription + implements WebSocketClient, WebSocketListener<TXID> +{ + static readonly PATH: HttpPath = { path: '/subscriptions/txpool' }; + + private readonly listeners: Array<WebSocketListener<TXID>> = []; + + private readonly wsc: WebSocketClient; + + protected constructor(wsc: WebSocketClient) { + this.wsc = wsc; + } + + addListener(listener: WebSocketListener<TXID>): this { + this.listeners.push(listener); + return this; + } + + static at(wsc: WebSocketClient): NewTransactionSubscription { + return new NewTransactionSubscription(wsc); + } + + get baseURL(): string { + return this.wsc.baseURL; + } + + close(): this { + this.wsc.close(); + return this; + } + + onClose(event: Event): void { + this.listeners.forEach((listener) => { + listener.onClose(event); + }); + } + + onError(event: Event): void { + this.listeners.forEach((listener) => { + listener.onError(event); + }); + } + + onMessage(event: MessageEvent<unknown>): void { + const json = JSON.parse(event.data as string) as TXIDJSON; + const message = new MessageEvent<TXID>(event.type, { + data: new TXID(json) + }); + this.listeners.forEach((listener) => { + listener.onMessage(message); + }); + } + + onOpen(event: Event): void { + this.listeners.forEach((listener) => { + listener.onOpen(event); + }); + } + + open(): this { + this.wsc.addListener(this).open(NewTransactionSubscription.PATH); + return this; + } + + removeListener(listener: WebSocketListener<TXID>): this { + this.listeners.splice(this.listeners.indexOf(listener), 1); + return this; + } +} + +export { NewTransactionSubscription }; diff --git a/packages/net/src/thor/subscriptions/SubscriptionBeat2Response.ts b/packages/net/src/thor/subscriptions/SubscriptionBeat2Response.ts new file mode 100644 index 000000000..417551247 --- /dev/null +++ b/packages/net/src/thor/subscriptions/SubscriptionBeat2Response.ts @@ -0,0 +1,54 @@ +import { BlockId, HexUInt, Units, VTHO } from '@vechain/sdk-core'; +import { UInt } from '../../../../core'; + +class SubscriptionBeat2Response { + readonly gasLimit: VTHO; + readonly obsolete: boolean; + readonly number: UInt; + readonly id: BlockId; + readonly parentID: BlockId; + readonly timestamp: UInt; + readonly txsFeatures: UInt; + readonly bloom: HexUInt; + readonly k: UInt; + + constructor(json: SubscriptionBeat2ResponseJSON) { + this.gasLimit = VTHO.of(json.gasLimit, Units.wei); + this.obsolete = json.obsolete; + this.number = UInt.of(json.number); + this.id = BlockId.of(json.id); + this.parentID = BlockId.of(json.parentID); + this.timestamp = UInt.of(json.timestamp); + this.txsFeatures = UInt.of(json.txsFeatures); + this.bloom = HexUInt.of(json.bloom); + this.k = UInt.of(json.k); + } + + toJSON(): SubscriptionBeat2ResponseJSON { + return { + gasLimit: Number(this.gasLimit.wei), + obsolete: this.obsolete, + number: this.number.valueOf(), + id: this.id.toString(), + parentID: this.parentID.toString(), + timestamp: this.timestamp.valueOf(), + txsFeatures: this.txsFeatures.valueOf(), + bloom: this.bloom.toString(), + k: this.k.valueOf() + } satisfies SubscriptionBeat2ResponseJSON; + } +} + +interface SubscriptionBeat2ResponseJSON { + gasLimit: number; + obsolete: boolean; + number: number; + id: string; + parentID: string; + timestamp: number; + txsFeatures: number; + bloom: string; + k: number; +} + +export { SubscriptionBeat2Response, type SubscriptionBeat2ResponseJSON }; diff --git a/packages/net/src/thor/subscriptions/SubscriptionBlockResponse.ts b/packages/net/src/thor/subscriptions/SubscriptionBlockResponse.ts new file mode 100644 index 000000000..a72bf3278 --- /dev/null +++ b/packages/net/src/thor/subscriptions/SubscriptionBlockResponse.ts @@ -0,0 +1,90 @@ +import { UInt, type TxId } from '../../../../core'; +import { Address, BlockId, ThorId, Units, VTHO } from '@vechain/sdk-core'; + +class SubscriptionBlockResponse { + readonly number: UInt; + readonly id: BlockId; + readonly size: UInt; + readonly parentID: BlockId; + readonly timestamp: UInt; + readonly gasLimit: VTHO; + readonly beneficiary: Address; + readonly gasUsed: VTHO; + readonly totalScore: UInt; + readonly txsRoot: ThorId; + readonly txsFeatures: UInt; + readonly stateRoot: ThorId; + readonly receiptsRoot: ThorId; + readonly com: boolean; + readonly signer: Address; + readonly obsolete: boolean; + readonly transactions: TxId[]; + + constructor(json: SubscriptionBlockResponseJSON) { + this.number = UInt.of(json.number); + this.id = BlockId.of(json.id); + this.size = UInt.of(json.size); + this.parentID = BlockId.of(json.parentID); + this.timestamp = UInt.of(json.timestamp); + this.gasLimit = VTHO.of(json.gasLimit, Units.wei); + this.beneficiary = Address.of(json.beneficiary); + this.gasUsed = VTHO.of(json.gasUsed, Units.wei); + this.totalScore = UInt.of(json.totalScore); + this.txsRoot = ThorId.of(json.txsRoot); + this.txsFeatures = UInt.of(json.txsFeatures); + this.stateRoot = ThorId.of(json.stateRoot); + this.receiptsRoot = ThorId.of(json.receiptsRoot); + this.com = json.com; + this.signer = Address.of(json.signer); + this.obsolete = json.obsolete; + this.transactions = json.transactions.map( + (txId: string): TxId => ThorId.of(txId) + ); + } + + toJSON(): SubscriptionBlockResponseJSON { + return { + number: this.number.valueOf(), + id: this.id.toString(), + size: this.size.valueOf(), + parentID: this.parentID.toString(), + timestamp: this.timestamp.valueOf(), + gasLimit: Number(this.gasLimit.wei), + beneficiary: this.beneficiary.toString(), + gasUsed: Number(this.gasUsed.wei), + totalScore: this.totalScore.valueOf(), + txsRoot: this.txsRoot.toString(), + txsFeatures: this.txsFeatures.valueOf(), + stateRoot: this.stateRoot.toString(), + receiptsRoot: this.receiptsRoot.toString(), + com: this.com, + signer: this.signer.toString(), + obsolete: this.obsolete, + transactions: this.transactions.map((txId: ThorId) => + txId.toString() + ) + } satisfies SubscriptionBlockResponseJSON; + } +} + +interface SubscriptionBlockResponseJSON { + number: number; + id: string; + size: number; + parentID: string; + timestamp: number; + gasLimit: number; + beneficiary: string; + gasUsed: number; + totalScore: number; + txsRoot: string; + txsFeatures: number; + stateRoot: string; + receiptsRoot: string; + com: boolean; + signer: string; + obsolete: boolean; + transactions: string[]; +} + +export { SubscriptionBlockResponse, type SubscriptionBlockResponseJSON }; diff --git a/packages/net/src/thor/subscriptions/SubscriptionEventResponse.ts b/packages/net/src/thor/subscriptions/SubscriptionEventResponse.ts new file mode 100644 index 000000000..ccd0db17b --- /dev/null +++ b/packages/net/src/thor/subscriptions/SubscriptionEventResponse.ts @@ -0,0 +1,42 @@ +import { LogMeta, type LogMetaJSON } from '../logs'; +import { Address, HexUInt, ThorId } from '@vechain/sdk-core'; + +class SubscriptionEventResponse { + readonly address: Address; + readonly topics: ThorId[]; + readonly data: HexUInt; + readonly obsolete: boolean; + readonly meta: LogMeta; + + constructor(json: SubscriptionEventResponseJSON) { + this.address = Address.of(json.address); + this.topics = json.topics.map( + (topic: string): ThorId => ThorId.of(topic) + ); + this.data = HexUInt.of(json.data); + this.obsolete = json.obsolete; + this.meta = new LogMeta(json.meta); + } + + toJSON(): SubscriptionEventResponseJSON { + return { + address: this.address.toString(), + topics: this.topics.map((topic: ThorId): string => + topic.toString() + ), + data: this.data.toString(), + obsolete: this.obsolete, + meta: this.meta.toJSON() + }; + } +} + +interface SubscriptionEventResponseJSON { + address: string; + topics: string[]; + data: string; + obsolete: boolean; + meta: LogMetaJSON; +} + +export { SubscriptionEventResponse, type SubscriptionEventResponseJSON }; diff --git a/packages/net/src/thor/subscriptions/SubscriptionTransferResponse.ts b/packages/net/src/thor/subscriptions/SubscriptionTransferResponse.ts new file mode 100644 index 000000000..7d1922db6 --- /dev/null +++ b/packages/net/src/thor/subscriptions/SubscriptionTransferResponse.ts @@ -0,0 +1,38 @@ +import { LogMeta, type LogMetaJSON } from '../logs'; +import { Address, HexUInt, Units, VET } from '@vechain/sdk-core'; + +class SubscriptionTransferResponse { + readonly sender: Address; + readonly recipient: Address; + readonly amount: VET; + readonly obsolete: boolean; + readonly meta: LogMeta; + + constructor(json: SubscriptionTransferJSON) { + this.sender = Address.of(json.sender); + this.recipient = Address.of(json.recipient); + this.amount = VET.of(HexUInt.of(json.amount).bi, Units.wei); + this.obsolete = json.obsolete; + this.meta = new LogMeta(json.meta); + } + + toJSON(): SubscriptionTransferJSON { + return { + sender: this.sender.toString(), + recipient: this.recipient.toString(), + amount: HexUInt.of(this.amount.wei).toString(), + obsolete: this.obsolete, + meta: this.meta.toJSON() + } satisfies SubscriptionTransferJSON; + } +} + +interface SubscriptionTransferJSON { + sender: string; + recipient: string; + amount: string; + obsolete: boolean; + meta: LogMetaJSON; +} + +export { SubscriptionTransferResponse, type SubscriptionTransferJSON }; diff --git a/packages/net/src/thor/subscriptions/TransfersSubscription.ts b/packages/net/src/thor/subscriptions/TransfersSubscription.ts new file mode 100644 index 000000000..8e4d6c009 --- /dev/null +++ b/packages/net/src/thor/subscriptions/TransfersSubscription.ts @@ -0,0 +1,128 @@ +import { type HttpPath, type HttpQuery } from '../../http'; +import { type Address, type BlockId } from '@vechain/sdk-core'; +import { type WebSocketClient, type WebSocketListener } from '../../ws'; +import { type SubscriptionTransferResponse } from './SubscriptionTransferResponse'; + +class TransfersSubscription + implements WebSocketClient, WebSocketListener<SubscriptionTransferResponse> +{ + static readonly PATH: HttpPath = { path: '/subscriptions/transfer' }; + + private readonly listeners: Array< + WebSocketListener<SubscriptionTransferResponse> + > = []; + + private readonly query: TransfersSubscriptionQuery; + + private readonly wsc: WebSocketClient; + + protected constructor( + wsc: WebSocketClient, + query: TransfersSubscriptionQuery + ) { + this.wsc = wsc; + this.query = query; + } + + addListener(listener: WebSocketListener<unknown>): this { + this.listeners.push(listener); + return this; + } + + close(): this { + this.wsc.close(); + return this; + } + + open(): this { + this.wsc.addListener(this).open({ + path: TransfersSubscription.PATH.path + this.query.query + }); + return this; + } + + removeListener(listener: WebSocketListener<unknown>): this { + this.listeners.splice(this.listeners.indexOf(listener), 1); + return this; + } + + static at(wsc: WebSocketClient): TransfersSubscription { + return new TransfersSubscription(wsc, new TransfersSubscriptionQuery()); + } + + get baseURL(): string { + return this.wsc.baseURL; + } + + onClose(event: Event): void { + this.listeners.forEach((listener) => { + listener.onClose(event); + }); + } + + onError(event: Event): void { + this.listeners.forEach((listener) => { + listener.onError(event); + }); + } + + onMessage(event: MessageEvent<unknown>): void { + const json = JSON.parse( + event.data as string + ) as SubscriptionTransferResponse; + const message = new MessageEvent<SubscriptionTransferResponse>( + event.type, + { data: json } + ); + this.listeners.forEach((listener) => { + listener.onMessage(message); + }); + } + + onOpen(event: Event): void { + this.listeners.forEach((listener) => { + listener.onOpen(event); + }); + } +} + +class TransfersSubscriptionQuery implements HttpQuery { + readonly pos?: BlockId; + readonly recipient?: Address; + readonly sender?: Address; + readonly txOrigin?: Address; + + constructor( + pos?: BlockId, + recipient?: Address, + sender?: Address, + txOrigin?: Address + ) { + this.pos = pos; + this.recipient = recipient; + this.sender = sender; + this.txOrigin = txOrigin; + } + + get query(): string { + let query = ''; + if (this.pos !== undefined) { + query += `pos=${this.pos}`; + } + if (this.recipient !== undefined) { + if (query.length > 0) query += '&'; + query += `recipient=${this.recipient}`; + } + if (this.sender !== undefined) { + if (query.length > 0) query += '&'; + query += `sender=${this.sender}`; + } + if (this.txOrigin !== undefined) { + if (query.length > 0) query += '&'; + query += `txOrigin=${this.txOrigin}`; + } + return query.length > 0 ? `?${query}` : query; + } +} + +export { TransfersSubscription }; diff --git a/packages/net/src/thor/subscriptions/index.ts b/packages/net/src/thor/subscriptions/index.ts new file mode 100644 index 000000000..4f8935c99 --- /dev/null +++ b/packages/net/src/thor/subscriptions/index.ts @@ -0,0 +1,9 @@ +export * from './BeatsSubscription'; +export * from './BlocksSubscription'; +export * from './EventsSubscription'; +export * from './NewTransactionSubscription'; +export * from './SubscriptionBeat2Response'; +export * from './SubscriptionBlockResponse'; +export * from './SubscriptionEventResponse'; +export * from './SubscriptionTransferResponse'; +export * from './TransfersSubscription'; diff --git a/packages/net/src/thor/transactions/Clause.ts b/packages/net/src/thor/transactions/Clause.ts new file mode 100644 index 000000000..936115770 --- /dev/null +++ b/packages/net/src/thor/transactions/Clause.ts @@ -0,0 +1,35 @@ +import { Address, HexUInt, VET } from '@vechain/sdk-core'; + +class Clause { + readonly to?: Address; + readonly value: VET; + readonly data: HexUInt; + + constructor(json: ClauseJSON) { + this.to = + json.to !== undefined && json.to != null + ? Address.of(json.to) + : undefined; + this.value = VET.of(json.value); + this.data = HexUInt.of(json.data); + } + + toJSON(): ClauseJSON { + return { + to: + this.to !== undefined && this.to !== null + ? this.to.toString() + : undefined, + value: HexUInt.of(this.value.wei).toString(), + data: this.data.toString() + } satisfies ClauseJSON; + } +} + +interface ClauseJSON { + to?: string | null; + value: string; + data: string; +} + +export { Clause, type ClauseJSON }; diff --git a/packages/net/src/thor/transactions/Event.ts b/packages/net/src/thor/transactions/Event.ts new file mode 100644 index 000000000..0336c0cf7 --- /dev/null +++ b/packages/net/src/thor/transactions/Event.ts @@ -0,0 +1,33 @@ +import { Address, HexUInt, ThorId } from '@vechain/sdk-core'; + +class Event { + readonly address: Address; + readonly topics: ThorId[]; + readonly data: HexUInt; + + constructor(json: EventJSON) { + this.address = Address.of(json.address); + this.topics = json.topics.map( + (topic: string): ThorId => ThorId.of(topic) + ); + this.data = HexUInt.of(json.data); + } + + toJSON(): EventJSON { + return { + address: this.address.toString(), + topics: this.topics.map((topic: ThorId): string => + topic.toString() + ), + data: this.data.toString() + } satisfies EventJSON; + } +} + +interface EventJSON { + address: string; + topics: string[]; + data: string; +} + +export { Event, type EventJSON }; diff --git a/packages/net/src/thor/transactions/GetRawTxResponse.ts b/packages/net/src/thor/transactions/GetRawTxResponse.ts new file mode 100644 index 000000000..3546620c9 --- /dev/null +++ b/packages/net/src/thor/transactions/GetRawTxResponse.ts @@ -0,0 +1,26 @@ +import { HexUInt } from '@vechain/sdk-core'; +import { TxMeta, type TxMetaJSON } from './TxMeta'; + +class GetRawTxResponse { + readonly raw: HexUInt; + readonly meta: TxMeta; + + constructor(json: GetRawTxResponseJSON) { + this.raw = HexUInt.of(json.raw); + this.meta = new TxMeta(json.meta); + } + + toJSON(): GetRawTxResponseJSON { + return { + raw: this.raw.toString(), + meta: this.meta.toJSON() + } satisfies GetRawTxResponseJSON; + } +} + +interface GetRawTxResponseJSON { + raw: string; + meta: TxMetaJSON; +} + +export { GetRawTxResponse, type GetRawTxResponseJSON }; diff --git a/packages/net/src/thor/transactions/GetTxReceiptResponse.ts b/packages/net/src/thor/transactions/GetTxReceiptResponse.ts new file mode 100644 index 000000000..6cd583f98 --- /dev/null +++ b/packages/net/src/thor/transactions/GetTxReceiptResponse.ts @@ -0,0 +1,24 @@ +import { Receipt, type ReceiptJSON } from './Receipt'; +import { ReceiptMeta, type ReceiptMetaJSON } from './ReceiptMeta'; + +class GetTxReceiptResponse extends Receipt { + readonly meta: ReceiptMeta; + + constructor(json: GetTxReceiptResponseJSON) { + super(json); + this.meta = new ReceiptMeta(json.meta); + } + + toJSON(): GetTxReceiptResponseJSON { + return { + ...super.toJSON(), + meta: this.meta.toJSON() + } satisfies GetTxReceiptResponseJSON; + } +} + +interface GetTxReceiptResponseJSON extends ReceiptJSON { + meta: ReceiptMetaJSON; +} + +export { GetTxReceiptResponse, type GetTxReceiptResponseJSON }; diff --git a/packages/net/src/thor/transactions/GetTxResponse.ts b/packages/net/src/thor/transactions/GetTxResponse.ts new file mode 100644 index 000000000..6c2c0d41e --- /dev/null +++ b/packages/net/src/thor/transactions/GetTxResponse.ts @@ -0,0 +1,85 @@ +import { Clause, type ClauseJSON } from './Clause'; +import { TxMeta, type TxMetaJSON } from './TxMeta'; +import { Address, BlockId, VTHO } from '@vechain/sdk-core'; +import { UInt } from '../../../../core/src'; + +import { Nonce } from '../../../../core/src/vcdm/Nonce'; +import { TxId } from '../../../../core/src/vcdm/BlockId'; + +class GetTxResponse { + readonly id: TxId; + readonly origin: Address; + readonly delegator: Address | null; + readonly size: UInt; + readonly chainTag: UInt; + readonly blockRef: BlockId; + readonly expiration: UInt; + readonly clauses: Clause[]; + readonly gasPriceCoef: UInt; + readonly gas: VTHO; + readonly dependsOn?: TxId | null; + readonly nonce: Nonce; + readonly meta: TxMeta; + + constructor(json: GetTxResponseJSON) { + this.id = TxId.of(json.id); + this.origin = Address.of(json.origin); + this.delegator = + json.delegator !== null ? Address.of(json.delegator) : null; + this.size = UInt.of(json.size); + this.chainTag = UInt.of(json.chainTag); + this.blockRef = BlockId.of(json.blockRef); + this.expiration = UInt.of(json.expiration); + this.clauses = json.clauses.map((clauseJSON: ClauseJSON) => { + return new Clause(clauseJSON); + }); + this.gasPriceCoef = UInt.of(json.gasPriceCoef); + this.gas = VTHO.of(json.gas); + this.dependsOn = + json.dependsOn !== undefined && json.dependsOn !== null + ? TxId.of(json.dependsOn) + : undefined; + this.nonce = Nonce.of(json.nonce); + this.meta = new TxMeta(json.meta); + } + + toJSON(): GetTxResponseJSON { + return { + id: this.id.toString(), + origin: this.origin.toString(), + delegator: + this.delegator != null ? this.delegator.toString() : null, + size: this.size.valueOf(), + chainTag: this.chainTag.valueOf(), + blockRef: this.blockRef.toString(), + expiration: this.expiration.valueOf(), + clauses: this.clauses?.map((clause) => clause.toJSON()), + gasPriceCoef: this.gasPriceCoef.valueOf(), + gas: Number(this.gas.wei), + dependsOn: + this.dependsOn !== undefined && this.dependsOn !== null + ? this.dependsOn.toString() + : undefined, + nonce: this.nonce.toString(), + meta: this.meta.toJSON() + } satisfies GetTxResponseJSON; + } +} + +interface GetTxResponseJSON { + id: string; + origin: string; + delegator: string | null; // The end point at https://mainnet.vechain.org/doc/stoplight-ui/#/schemas/GetTxResponse specifically returns `null`. + size: number; + chainTag: number; + blockRef: string; + expiration: number; + clauses: ClauseJSON[]; + gasPriceCoef: number; + gas: number; + dependsOn?: string | null; + nonce: string; + meta: TxMetaJSON; +} + +export { GetTxResponse, type GetTxResponseJSON }; diff --git a/packages/net/src/thor/transactions/Receipt.ts b/packages/net/src/thor/transactions/Receipt.ts new file mode 100644 index 000000000..40ac4da90 --- /dev/null +++ b/packages/net/src/thor/transactions/Receipt.ts @@ -0,0 +1,42 @@ +import { ReceiptOutput, type ReceiptOutputJSON } from './ReceiptOutput'; +import { Address, Hex, HexUInt, Units, VTHO } from '@vechain/sdk-core'; + +class Receipt { + readonly gasUsed: VTHO; + readonly gasPayer: Address; + readonly paid: VTHO; + readonly reward: VTHO; + readonly reverted: boolean; + readonly outputs: ReceiptOutput[]; + + constructor(json: ReceiptJSON) { + this.gasUsed = VTHO.of(json.gasUsed); + this.gasPayer = Address.of(json.gasPayer); + this.paid = VTHO.of(Hex.of(json.paid).bi, Units.wei); + this.reward = VTHO.of(Hex.of(json.reward).bi, Units.wei); + this.reverted = json.reverted; + this.outputs = json.outputs.map((output) => new ReceiptOutput(output)); + } + + toJSON(): ReceiptJSON { + return { + gasUsed: Number(this.gasUsed.wei), + gasPayer: this.gasPayer.toString(), + paid: HexUInt.of(this.paid.wei).toString(), + reward: HexUInt.of(this.reward.wei).toString(), + reverted: this.reverted, + outputs: this.outputs.map((output) => output.toJSON()) + }; + } +} + +interface ReceiptJSON { + gasUsed: number; + gasPayer: string; + paid: string; + reward: string; + reverted: boolean; + outputs: ReceiptOutputJSON[]; +} + +export { Receipt, type ReceiptJSON }; diff --git a/packages/net/src/thor/transactions/ReceiptMeta.ts b/packages/net/src/thor/transactions/ReceiptMeta.ts new file mode 100644 index 000000000..47216cf0d --- /dev/null +++ b/packages/net/src/thor/transactions/ReceiptMeta.ts @@ -0,0 +1,30 @@ +import { TxMeta, type TxMetaJSON } from './TxMeta'; +import { Address } from '@vechain/sdk-core'; + +import { TxId } from '../../../../core/src/vcdm/BlockId'; + +class ReceiptMeta extends TxMeta { + readonly txID: TxId; + readonly txOrigin: Address; + + constructor(json: ReceiptMetaJSON) { + super(json); + this.txID = TxId.of(json.txID); + this.txOrigin = Address.of(json.txOrigin); + } + + toJSON(): ReceiptMetaJSON { + return { + ...super.toJSON(), + txID: this.txID.toString(), + txOrigin: this.txOrigin.toString() + } satisfies ReceiptMetaJSON; + } +} + +interface ReceiptMetaJSON extends TxMetaJSON { + txID: string; + txOrigin: string; +} + +export { ReceiptMeta, type ReceiptMetaJSON }; diff --git a/packages/net/src/thor/transactions/ReceiptOutput.ts b/packages/net/src/thor/transactions/ReceiptOutput.ts new file mode 100644 index 000000000..01dbde469 --- /dev/null +++ b/packages/net/src/thor/transactions/ReceiptOutput.ts @@ -0,0 +1,37 @@ +import { Event, type EventJSON } from './Event'; +import { Transfer, type TransferJSON } from './Transfer'; +import { Address } from '@vechain/sdk-core'; + +class ReceiptOutput { + readonly contractAddress: Address; + readonly events: Event[]; + readonly transfers: Transfer[]; + + constructor(json: ReceiptOutputJSON) { + this.contractAddress = Address.of(json.contractAddress); + this.events = json.events.map( + (eventJSON): Event => new Event(eventJSON) + ); + this.transfers = json.transfers.map( + (transferJSON): Transfer => new Transfer(transferJSON) + ); + } + + toJSON(): ReceiptOutputJSON { + return { + contractAddress: this.contractAddress.toString(), + events: this.events.map((event): EventJSON => event.toJSON()), + transfers: this.transfers.map( + (transfer): TransferJSON => transfer.toJSON() + ) + } satisfies ReceiptOutputJSON; + } +} + +interface ReceiptOutputJSON { + contractAddress: string; + events: EventJSON[]; + transfers: TransferJSON[]; +} + +export { ReceiptOutput, type ReceiptOutputJSON }; diff --git a/packages/net/src/thor/transactions/RetrieveRawTransactionByID.ts b/packages/net/src/thor/transactions/RetrieveRawTransactionByID.ts new file mode 100644 index 000000000..308098d0a --- /dev/null +++ b/packages/net/src/thor/transactions/RetrieveRawTransactionByID.ts @@ -0,0 +1,77 @@ +import { + RetrieveTransactionByIDPath, + RetrieveTransactionByIDQuery +} from './RetrieveTransactionByID'; +import { + GetRawTxResponse, + type GetRawTxResponseJSON +} from './GetRawTxResponse'; +import { type BlockId } from '@vechain/sdk-core'; +import { type HttpClient } from '../../http'; +import { type ThorRequest } from '../ThorRequest'; +import { type ThorResponse } from '../ThorResponse'; + +import { type TxId } from '../../../../core/src/vcdm/BlockId'; + +class RetrieveRawTransactionByID + implements ThorRequest<RetrieveRawTransactionByID, GetRawTxResponse> +{ + readonly path: RetrieveRawTransactionByIDPath; + + readonly query: RetrieveRawTransactionByIDQuery; + + constructor( + path: RetrieveRawTransactionByIDPath, + query: RetrieveRawTransactionByIDQuery + ) { + this.path = path; + this.query = query; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<RetrieveRawTransactionByID, GetRawTxResponse>> { + const response = await httpClient.get(this.path, this.query); + const responseBody = (await response.json()) as GetRawTxResponseJSON; + return { + request: this, + response: new GetRawTxResponse(responseBody) + }; + } + + static of(txId: TxId): RetrieveRawTransactionByID { + return new RetrieveRawTransactionByID( + new RetrieveRawTransactionByIDPath(txId), + new RetrieveRawTransactionByIDQuery(undefined, false) + ); + } + + withHead(head?: BlockId): RetrieveRawTransactionByID { + return new RetrieveRawTransactionByID( + this.path, + new RetrieveRawTransactionByIDQuery(head, this.query.pending) + ); + } + + withPending(pending: boolean = true): RetrieveRawTransactionByID { + return new RetrieveRawTransactionByID( + this.path, + new RetrieveRawTransactionByIDQuery(this.query.head, pending) + ); + } +} + +class RetrieveRawTransactionByIDPath extends RetrieveTransactionByIDPath {} + +class RetrieveRawTransactionByIDQuery extends RetrieveTransactionByIDQuery { + get query(): string { + const head = this.head === null ? '' : `${this.head}&`; + return `?${head}pending=${this.pending}&raw=true`; + } +} + +export { + RetrieveRawTransactionByID, + RetrieveRawTransactionByIDPath, + RetrieveRawTransactionByIDQuery +}; diff --git a/packages/net/src/thor/transactions/RetrieveTransactionByID.ts b/packages/net/src/thor/transactions/RetrieveTransactionByID.ts new file mode 100644 index 000000000..1ae1abc32 --- /dev/null +++ b/packages/net/src/thor/transactions/RetrieveTransactionByID.ts @@ -0,0 +1,88 @@ +import { type BlockId } from '@vechain/sdk-core'; +import { type HttpClient, type HttpPath, type HttpQuery } from '../../http'; +import { GetTxResponse, type GetTxResponseJSON } from './GetTxResponse'; +import { type ThorRequest } from '../ThorRequest'; +import { type ThorResponse } from '../ThorResponse'; + +import { type TxId } from '../../../../core/src/vcdm/BlockId'; + +class RetrieveTransactionByID + implements ThorRequest<RetrieveTransactionByID, GetTxResponse> +{ + readonly path: RetrieveTransactionByIDPath; + + readonly query: RetrieveTransactionByIDQuery; + + constructor( + path: RetrieveTransactionByIDPath, + query: RetrieveTransactionByIDQuery + ) { + this.path = path; + this.query = query; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<RetrieveTransactionByID, GetTxResponse>> { + const response = await httpClient.get(this.path, this.query); + const responseBody = (await response.json()) as GetTxResponseJSON; + return { + request: this, + response: new GetTxResponse(responseBody) + }; + } + + static of(txId: TxId): RetrieveTransactionByID { + return new RetrieveTransactionByID( + new RetrieveTransactionByIDPath(txId), + new RetrieveTransactionByIDQuery(undefined, false) + ); + } + + withHead(head?: BlockId): RetrieveTransactionByID { + return new RetrieveTransactionByID( + this.path, + new RetrieveTransactionByIDQuery(head, this.query.pending) + ); + } + + withPending(pending: boolean = true): RetrieveTransactionByID { + return new RetrieveTransactionByID( + this.path, + new RetrieveTransactionByIDQuery(this.query.head, pending) + ); + } +} + +class RetrieveTransactionByIDPath implements HttpPath { + readonly txId: TxId; + + constructor(txId: TxId) { + this.txId = txId; + } + + get path(): string { + return `/transactions/${this.txId}`; + } +} + +class RetrieveTransactionByIDQuery implements HttpQuery { + readonly head?: BlockId; + readonly pending: boolean; + + constructor(head: BlockId | undefined, pending: boolean) { + this.head = head; + this.pending = pending; + } + + get query(): string { + const head = this.head === undefined ? '' : `${this.head}&`; + return `?${head}pending=${this.pending}&raw=false`; + } +} + +export { + RetrieveTransactionByID, + RetrieveTransactionByIDPath, + RetrieveTransactionByIDQuery +}; diff --git a/packages/net/src/thor/transactions/RetrieveTransactionReceipt.ts b/packages/net/src/thor/transactions/RetrieveTransactionReceipt.ts new file mode 100644 index 000000000..a89792b66 --- /dev/null +++ b/packages/net/src/thor/transactions/RetrieveTransactionReceipt.ts @@ -0,0 +1,85 @@ +import { type BlockId } from '@vechain/sdk-core'; +import { type HttpClient, type HttpPath, type HttpQuery } from '../../http'; +import { + GetTxReceiptResponse, + type GetTxReceiptResponseJSON +} from './GetTxReceiptResponse'; +import { type ThorRequest } from '../ThorRequest'; +import { type ThorResponse } from '../ThorResponse'; + +import { type TxId } from '../../../../core/src/vcdm/BlockId'; + +class RetrieveTransactionReceipt + implements ThorRequest<RetrieveTransactionReceipt, GetTxReceiptResponse> +{ + readonly path: RetrieveTransactionReceiptPath; + + readonly query: RetrieveTransactionReceiptQuery; + + constructor( + path: RetrieveTransactionReceiptPath, + query: RetrieveTransactionReceiptQuery + ) { + this.path = path; + this.query = query; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<RetrieveTransactionReceipt, GetTxReceiptResponse>> { + const response = await httpClient.get(this.path, this.query); + const responseBody = + (await response.json()) as GetTxReceiptResponseJSON; + return { + request: this, + response: new GetTxReceiptResponse(responseBody) + } satisfies ThorResponse< + RetrieveTransactionReceipt, + GetTxReceiptResponse + >; + } + + static of(txId: TxId): RetrieveTransactionReceipt { + return new RetrieveTransactionReceipt( + new RetrieveTransactionReceiptPath(txId), + new RetrieveTransactionReceiptQuery(undefined) + ); + } + + withHead(head?: BlockId): RetrieveTransactionReceipt { + return new RetrieveTransactionReceipt( + this.path, + new RetrieveTransactionReceiptQuery(head) + ); + } +} + +class RetrieveTransactionReceiptPath implements HttpPath { + readonly txId: TxId; + + constructor(txId: TxId) { + this.txId = txId; + } + + get path(): string { + return `/transactions/${this.txId}/receipt`; + } +} + +class RetrieveTransactionReceiptQuery implements HttpQuery { + readonly head?: BlockId; + + constructor(head: BlockId | undefined) { + this.head = head; + } + + get query(): string { + return this.head === undefined ? '' : `${this.head}&`; + } +} + +export { + RetrieveTransactionReceipt, + RetrieveTransactionReceiptPath, + RetrieveTransactionReceiptQuery +}; diff --git a/packages/net/src/thor/transactions/SendTransaction.ts b/packages/net/src/thor/transactions/SendTransaction.ts new file mode 100644 index 000000000..4f4aa6fc3 --- /dev/null +++ b/packages/net/src/thor/transactions/SendTransaction.ts @@ -0,0 +1,38 @@ +import { HexUInt } from '@vechain/sdk-core'; +import { type HttpClient, type HttpPath } from '../../http'; +import { type ThorRequest } from '../ThorRequest'; +import { type ThorResponse } from '../ThorResponse'; +import { TXID, type TXIDJSON } from './TXID'; + +class SendTransaction implements ThorRequest<SendTransaction, TXID> { + static readonly PATH: HttpPath = { path: '/transactions' }; + + readonly encoded: Uint8Array; + + constructor(encoded: Uint8Array) { + this.encoded = encoded; + } + + async askTo( + httpClient: HttpClient + ): Promise<ThorResponse<SendTransaction, TXID>> { + const response = await httpClient.post( + SendTransaction.PATH, + { query: '' }, + { + raw: HexUInt.of(this.encoded).toString() + } + ); + const json = (await response.json()) as TXIDJSON; + return { + request: this, + response: new TXID(json) + } satisfies ThorResponse<SendTransaction, TXID>; + } + + static of(encoded: Uint8Array): SendTransaction { + return new SendTransaction(encoded); + } +} + +export { SendTransaction }; diff --git a/packages/net/src/thor/transactions/TXID.ts b/packages/net/src/thor/transactions/TXID.ts new file mode 100644 index 000000000..d30f999d8 --- /dev/null +++ b/packages/net/src/thor/transactions/TXID.ts @@ -0,0 +1,21 @@ +import { ThorId } from '@vechain/sdk-core'; + +class TXID { + readonly id: ThorId; + + constructor(json: TXIDJSON) { + this.id = ThorId.of(json.id); + } + + toJSON(): TXIDJSON { + return { + id: this.id.toString() + } satisfies TXIDJSON; + } +} + +interface TXIDJSON { + id: string; +} + +export { TXID, type TXIDJSON }; diff --git a/packages/net/src/thor/transactions/Transfer.ts b/packages/net/src/thor/transactions/Transfer.ts new file mode 100644 index 000000000..ea63164d1 --- /dev/null +++ b/packages/net/src/thor/transactions/Transfer.ts @@ -0,0 +1,29 @@ +import { Address, VET } from '@vechain/sdk-core'; + +class Transfer { + readonly sender: Address; + readonly recipient: Address; + readonly amount: VET; + + constructor(json: TransferJSON) { + this.sender = Address.of(json.sender); + this.recipient = Address.of(json.recipient); + this.amount = VET.of(json.amount); + } + + toJSON(): TransferJSON { + return { + sender: this.sender.toString(), + recipient: this.recipient.toString(), + amount: this.amount.toString() + } satisfies TransferJSON; + } +} + +interface TransferJSON { + sender: string; + recipient: string; + amount: string; +} + +export { Transfer, type TransferJSON }; diff --git a/packages/net/src/thor/transactions/TxMeta.ts b/packages/net/src/thor/transactions/TxMeta.ts new file mode 100644 index 000000000..589a4ec75 --- /dev/null +++ b/packages/net/src/thor/transactions/TxMeta.ts @@ -0,0 +1,31 @@ +import { BlockId } from '@vechain/sdk-core'; + +import { UInt } from '../../../../core/src/vcdm/UInt'; + +class TxMeta { + readonly blockID: BlockId; + readonly blockNumber: UInt; + readonly blockTimestamp: bigint; + + constructor(json: TxMetaJSON) { + this.blockID = BlockId.of(json.blockID); + this.blockNumber = UInt.of(json.blockNumber); + this.blockTimestamp = json.blockTimestamp; + } + + toJSON(): TxMetaJSON { + return { + blockID: this.blockID.toString(), + blockNumber: this.blockNumber.valueOf(), + blockTimestamp: this.blockTimestamp + } satisfies TxMetaJSON; + } +} + +interface TxMetaJSON { + blockID: string; + blockNumber: number; + blockTimestamp: bigint; +} + +export { TxMeta, type TxMetaJSON }; diff --git a/packages/net/src/thor/transactions/index.ts b/packages/net/src/thor/transactions/index.ts new file mode 100644 index 000000000..65504bc01 --- /dev/null +++ b/packages/net/src/thor/transactions/index.ts @@ -0,0 +1,15 @@ +export * from './Clause'; +export * from './Event'; +export * from './GetRawTxResponse'; +export * from './GetTxResponse'; +export * from './GetTxReceiptResponse'; +export * from './Receipt'; +export * from './ReceiptMeta'; +export * from './ReceiptOutput'; +export * from './RetrieveRawTransactionByID'; +export * from './RetrieveTransactionByID'; +export * from './RetrieveTransactionReceipt'; +export * from './SendTransaction'; +export * from './TXID'; +export * from './Transfer'; +export * from './TxMeta'; diff --git a/packages/net/src/ws/MozillaWebSocketClient.ts b/packages/net/src/ws/MozillaWebSocketClient.ts new file mode 100644 index 000000000..74e2929e1 --- /dev/null +++ b/packages/net/src/ws/MozillaWebSocketClient.ts @@ -0,0 +1,60 @@ +import { type WebSocketClient } from './WebSocketClient'; +import { type WebSocketListener } from './WebSocketListener'; +import { type HttpPath } from '../http'; + +class MozillaWebSocketClient implements WebSocketClient { + readonly baseURL: string; + + private ws?: WebSocket; + + private readonly listeners: Array<WebSocketListener<unknown>> = []; + + constructor(baseURL: string) { + this.baseURL = baseURL; + } + + addListener(listener: WebSocketListener<unknown>): this { + this.listeners.push(listener); + return this; + } + + close(): this { + this.ws?.close(); + this.ws = undefined; + return this; + } + + open(path: HttpPath): this { + this.close(); + this.ws = new WebSocket(this.baseURL + path.path); + this.ws.onopen = (event: Event) => { + this.listeners.forEach((listener) => { + listener.onOpen?.(event); + }); + }; + this.ws.onerror = (event: Event) => { + this.listeners.forEach((listener) => { + listener.onError?.(event); + }); + }; + this.ws.onmessage = (event: MessageEvent<unknown>) => { + this.listeners.forEach((listener) => { + listener.onMessage?.(event); + }); + }; + this.ws.onclose = (event: CloseEvent) => { + this.listeners.forEach((listener) => { + listener.onClose?.(event); + }); + this.ws = undefined; + }; + return this; + } + + removeListener(listener: WebSocketListener<unknown>): this { + this.listeners.splice(this.listeners.indexOf(listener), 1); + return this; + } +} + +export { MozillaWebSocketClient }; diff --git a/packages/net/src/ws/WebSocketClient.ts b/packages/net/src/ws/WebSocketClient.ts new file mode 100644 index 000000000..5df08ae7e --- /dev/null +++ b/packages/net/src/ws/WebSocketClient.ts @@ -0,0 +1,16 @@ +import type { WebSocketListener } from './WebSocketListener'; +import { type HttpPath } from '../http'; + +interface WebSocketClient { + get baseURL(): string; + + addListener: (listener: WebSocketListener<unknown>) => WebSocketClient; + + close: () => WebSocketClient; + + open: (path: HttpPath) => WebSocketClient; + + removeListener: (listener: WebSocketListener<unknown>) => WebSocketClient; +} + +export { type WebSocketClient }; diff --git a/packages/net/src/ws/WebSocketListener.ts b/packages/net/src/ws/WebSocketListener.ts new file mode 100644 index 000000000..3ca023fda --- /dev/null +++ b/packages/net/src/ws/WebSocketListener.ts @@ -0,0 +1,8 @@ +interface WebSocketListener<EventType> { + onClose: (event: Event) => void; + onError: (event: Event) => void; + onMessage: (event: MessageEvent<EventType>) => void; + onOpen: (event: Event) => void; +} + +export type { WebSocketListener }; diff --git a/packages/net/src/ws/index.ts b/packages/net/src/ws/index.ts new file mode 100644 index 000000000..2f3415242 --- /dev/null +++ b/packages/net/src/ws/index.ts @@ -0,0 +1,3 @@ +export * from './MozillaWebSocketClient'; +export * from './WebSocketClient'; +export * from './WebSocketListener'; diff --git a/packages/net/tests/http/FetchHttpClient.testnet.test.ts b/packages/net/tests/http/FetchHttpClient.testnet.test.ts new file mode 100644 index 000000000..b9c4d250b --- /dev/null +++ b/packages/net/tests/http/FetchHttpClient.testnet.test.ts @@ -0,0 +1,42 @@ +import { describe, test } from '@jest/globals'; +import { FetchHttpClient, ThorNetworks } from '../../src'; + +/** + * Test FetchHttpClient class. + * + * @group integration/network/http + */ +describe('FetchHttpClient testnet tests', () => { + test('ok <- get', async () => { + await new FetchHttpClient( + ThorNetworks.TESTNET, + (request: Request) => { + console.log(request); + return request; + }, + (response: Response) => { + console.log(response); + return response; + } + ).get(); + }); + + test('ok <- post', async () => { + const expected = { + hello: 'world' + }; + const response = await new FetchHttpClient( + 'https://httpbin.org', + (request: Request) => { + console.log(request); + return request; + }, + (response: Response) => { + console.log(response); + return response; + } + ).post({ path: '/post' }, { query: '' }, expected); + const actual: unknown = await response.json(); + console.log(JSON.stringify(actual, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/accounts/InspectClauses.testnet.test.ts b/packages/net/tests/thor/accounts/InspectClauses.testnet.test.ts new file mode 100644 index 000000000..bb0593b2a --- /dev/null +++ b/packages/net/tests/thor/accounts/InspectClauses.testnet.test.ts @@ -0,0 +1,41 @@ +import { describe, test } from '@jest/globals'; +import { + type ExecuteCodesRequestJSON, + FetchHttpClient, + InspectClauses, + ThorNetworks +} from '../../../src'; + +describe('InspectClauses testnet tests', () => { + test('ok <- askTo', async () => { + const request = { + gas: 50000, + gasPrice: '1000000000000000', + caller: '0x6d95e6dca01d109882fe1726a2fb9865fa41e7aa', + provedWork: '1000', + gasPayer: '0xd3ae78222beadb038203be21ed5ce7c9b1bff602', + expiration: 1000, + blockRef: '0x00000000851caf3c', + clauses: [ + { + to: '0x0000000000000000000000000000456E65726779', + value: '0x0', + data: '0xa9059cbb0000000000000000000000000f872421dc479f3c11edd89512731814d0598db50000000000000000000000000000000000000000000000013f306a2409fc0000' + }, + { + to: '0xf077b491b355E64048cE21E3A6Fc4751eEeA77fa', + value: '0x6124fee993bc00000', + data: '0x' + }, + { + value: '0x0', + data: '0x6080604052348015600f57600080fd5b50609f8061001e6000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680631820cabb146044575b600080fd5b348015604f57600080fd5b506056606c565b6040518082815260200191505060405180910390f35b62015180815600a165627a7a723058200ac7475da248e2fc26c057319e296e90c24d5f8b9bf956fb3b77545642cad3b10029' + } + ] + } satisfies ExecuteCodesRequestJSON; + const r = await InspectClauses.of(request).askTo( + FetchHttpClient.at(ThorNetworks.TESTNET) + ); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/accounts/RetrieveAccountDetails.testnet.test.ts b/packages/net/tests/thor/accounts/RetrieveAccountDetails.testnet.test.ts new file mode 100644 index 000000000..f5d663b4a --- /dev/null +++ b/packages/net/tests/thor/accounts/RetrieveAccountDetails.testnet.test.ts @@ -0,0 +1,16 @@ +import { describe, test } from '@jest/globals'; +import { + FetchHttpClient, + RetrieveAccountDetails, + ThorNetworks +} from '../../../src'; +import { Address } from '@vechain/sdk-core'; + +describe('RetrieveAccountDetails testnet tests', () => { + test('ok <- askTo', async () => { + const r = await RetrieveAccountDetails.of( + Address.of('0x0000000000000000000000000000456E65726779') + ).askTo(FetchHttpClient.at(ThorNetworks.TESTNET)); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/accounts/RetrieveContractBytecode.testnet.test.ts b/packages/net/tests/thor/accounts/RetrieveContractBytecode.testnet.test.ts new file mode 100644 index 000000000..dc3299953 --- /dev/null +++ b/packages/net/tests/thor/accounts/RetrieveContractBytecode.testnet.test.ts @@ -0,0 +1,16 @@ +import { describe, test } from '@jest/globals'; +import { Address } from '@vechain/sdk-core'; +import { + RetrieveContractBytecode, + FetchHttpClient, + ThorNetworks +} from '../../../src'; + +describe('RetrieveContractBytecode testnet tests', () => { + test('ok <- askTo', async () => { + const r = await RetrieveContractBytecode.of( + Address.of('0x0000000000000000000000000000456E65726779') + ).askTo(FetchHttpClient.at(ThorNetworks.TESTNET)); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/accounts/RetrieveStoragePositionValue.testnet.test.ts b/packages/net/tests/thor/accounts/RetrieveStoragePositionValue.testnet.test.ts new file mode 100644 index 000000000..73f135efe --- /dev/null +++ b/packages/net/tests/thor/accounts/RetrieveStoragePositionValue.testnet.test.ts @@ -0,0 +1,19 @@ +import { describe, test } from '@jest/globals'; +import { Address, BlockId } from '@vechain/sdk-core'; +import { + FetchHttpClient, + RetrieveStoragePositionValue, + ThorNetworks +} from '../../../src'; + +describe('RetrieveStoragePositionValue testnet tests', () => { + test('ok <- askTo', async () => { + const r = await RetrieveStoragePositionValue.of( + Address.of('0x93Ae8aab337E58A6978E166f8132F59652cA6C56'), + BlockId.of( + '0x0000000000000000000000000000000000000000000000000000000000000001' + ) + ).askTo(FetchHttpClient.at(ThorNetworks.TESTNET)); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/blocks/RetrieveBlock.testnet.test.ts b/packages/net/tests/thor/blocks/RetrieveBlock.testnet.test.ts new file mode 100644 index 000000000..320cccc7d --- /dev/null +++ b/packages/net/tests/thor/blocks/RetrieveBlock.testnet.test.ts @@ -0,0 +1,12 @@ +import { describe, test } from '@jest/globals'; +import { FetchHttpClient, RetrieveBlock, ThorNetworks } from '../../../src'; +import { Revision } from '@vechain/sdk-core'; + +describe('RetrieveBlock testnet tests', () => { + test('ok <- askTo', async () => { + const r = await RetrieveBlock.of(Revision.BEST).askTo( + FetchHttpClient.at(ThorNetworks.TESTNET) + ); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/debug/RetrieveStorageRange.mainnet.test.ts b/packages/net/tests/thor/debug/RetrieveStorageRange.mainnet.test.ts new file mode 100644 index 000000000..daba15595 --- /dev/null +++ b/packages/net/tests/thor/debug/RetrieveStorageRange.mainnet.test.ts @@ -0,0 +1,22 @@ +import { describe, test } from '@jest/globals'; +import { + RetrieveStorageRange, + type StorageRangeOptionJSON +} from '../../../src/thor/debug'; +import { FetchHttpClient, ThorNetworks } from '../../../src'; + +describe('RetrieveStorageRange mainnet tests', () => { + test('ok <- askTo', async () => { + const request = { + address: '0xd8ccdd85abdbf68dfec95f06c973e87b1b5a9997', + keyStart: + '0x0000000000000000000000000000000000000000000000000000000000000000', + maxResult: 10, + target: '0x010709463c1f0c9aa66a31182fb36d1977d99bfb6526bae0564a0eac4006c31a/0/0' + } satisfies StorageRangeOptionJSON; + const r = await RetrieveStorageRange.of(request).askTo( + FetchHttpClient.at(ThorNetworks.MAINNET) + ); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/debug/TraceCall.testnet.test.ts b/packages/net/tests/thor/debug/TraceCall.testnet.test.ts new file mode 100644 index 000000000..e06ad9722 --- /dev/null +++ b/packages/net/tests/thor/debug/TraceCall.testnet.test.ts @@ -0,0 +1,28 @@ +import { describe, test } from '@jest/globals'; +import { + type PostDebugTracerCallRequestJSON, + TraceCall +} from '../../../src/thor/debug'; +import { FetchHttpClient, ThorNetworks } from '../../../src'; + +describe('TraceCall testnet tests', () => { + test('ok <- askTo', async () => { + const request = { + value: '0x0', + to: '0x0000000000000000000000000000456E65726779', + data: '0xa9059cbb0000000000000000000000000f872421dc479f3c11edd89512731814d0598db50000000000', + gas: 50000, + gasPrice: '1000000000000000', + caller: '0x7567d83b7b8d80addcb281a71d54fc7b3364ffed', + provedWork: '1000', + gasPayer: '0xd3ae78222beadb038203be21ed5ce7c9b1bff602', + expiration: 1000, + blockRef: '0x00000000851caf3c', + name: 'call' + } satisfies PostDebugTracerCallRequestJSON; + const r = await TraceCall.of(request).askTo( + FetchHttpClient.at(ThorNetworks.TESTNET) + ); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/debug/TraceTransactionClause.mainnet.test.ts b/packages/net/tests/thor/debug/TraceTransactionClause.mainnet.test.ts new file mode 100644 index 000000000..85daa5aa6 --- /dev/null +++ b/packages/net/tests/thor/debug/TraceTransactionClause.mainnet.test.ts @@ -0,0 +1,20 @@ +import { describe, test } from '@jest/globals'; +import { + type PostDebugTracerRequestJSON, + TraceTransactionClause +} from '../../../src/thor/debug'; +import { FetchHttpClient, ThorNetworks } from '../../../src'; + +describe('TraceTransactionClause mainnet tests', () => { + test('ok <- askTo', async () => { + const request = { + target: '0x010709463c1f0c9aa66a31182fb36d1977d99bfb6526bae0564a0eac4006c31a/0/0', + name: 'call', + config: {} + } satisfies PostDebugTracerRequestJSON; + const r = await TraceTransactionClause.of(request).askTo( + FetchHttpClient.at(ThorNetworks.MAINNET) + ); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/logs/QuerySmartContractEvents.testnet.test.ts b/packages/net/tests/thor/logs/QuerySmartContractEvents.testnet.test.ts new file mode 100644 index 000000000..00bfa8cf9 --- /dev/null +++ b/packages/net/tests/thor/logs/QuerySmartContractEvents.testnet.test.ts @@ -0,0 +1,35 @@ +import { describe, test } from '@jest/globals'; + +import { + type EventLogFilterRequestJSON, + QuerySmartContractEvents +} from '../../../src/thor/logs'; +import { FetchHttpClient, ThorNetworks } from '../../../src'; + +describe('QuerySmartContractEvents testnet tests', () => { + test('ok <- askTo', async () => { + const request = { + range: { + unit: 'block', + from: 17240365, + to: 17289864 + }, + options: { + offset: 0, + limit: 100 + }, + criteriaSet: [ + { + address: '0x0000000000000000000000000000456E65726779', + topic0: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + topic1: '0x0000000000000000000000006d95e6dca01d109882fe1726a2fb9865fa41e7aa' + } + ], + order: 'asc' + } satisfies EventLogFilterRequestJSON; + const r = await QuerySmartContractEvents.of(request).askTo( + FetchHttpClient.at(ThorNetworks.TESTNET) + ); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/logs/QueryVETTransferEvents.testnet.test.ts b/packages/net/tests/thor/logs/QueryVETTransferEvents.testnet.test.ts new file mode 100644 index 000000000..2d82713df --- /dev/null +++ b/packages/net/tests/thor/logs/QueryVETTransferEvents.testnet.test.ts @@ -0,0 +1,32 @@ +import { describe, test } from '@jest/globals'; +import { type TransferLogFilterRequestJSON } from '../../../src/thor/logs/TransferLogFilterRequest'; +import { QueryVETTransferEvents } from '../../../src/thor/logs/QueryVETTransferEvents'; +import { FetchHttpClient, ThorNetworks } from '../../../src'; + +describe('QueryVETTransferEvents testnet tests', () => { + test('ok <- askTo', async () => { + const request = { + range: { + unit: 'block', + from: 17240365, + to: 17289864 + }, + options: { + offset: 0, + limit: 100 + }, + criteriaSet: [ + { + txOrigin: '0x6d95e6dca01d109882fe1726a2fb9865fa41e7aa', + sender: '0x6d95e6dca01d109882fe1726a2fb9865fa41e7aa', + recipient: '0x45429a2255e7248e57fce99e7239aed3f84b7a53' + } + ], + order: 'asc' + } satisfies TransferLogFilterRequestJSON; + const r = await QueryVETTransferEvents.of(request).askTo( + FetchHttpClient.at(ThorNetworks.TESTNET) + ); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/node/RetrieveConnectedPeers.testnet.test.ts b/packages/net/tests/thor/node/RetrieveConnectedPeers.testnet.test.ts new file mode 100644 index 000000000..96a771424 --- /dev/null +++ b/packages/net/tests/thor/node/RetrieveConnectedPeers.testnet.test.ts @@ -0,0 +1,15 @@ +import { describe, test } from '@jest/globals'; +import { + FetchHttpClient, + RetrieveConnectedPeers, + ThorNetworks +} from '../../../src'; + +describe('RetrieveConnectedPeers testnet tests', () => { + test('ok <- askTo', async () => { + const r = await new RetrieveConnectedPeers().askTo( + FetchHttpClient.at(ThorNetworks.TESTNET) + ); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/subscriptions/BeatsSubscription.solo.test.ts b/packages/net/tests/thor/subscriptions/BeatsSubscription.solo.test.ts new file mode 100644 index 000000000..28572b9ad --- /dev/null +++ b/packages/net/tests/thor/subscriptions/BeatsSubscription.solo.test.ts @@ -0,0 +1,43 @@ +import { afterEach, beforeEach, describe } from '@jest/globals'; +import { + MozillaWebSocketClient, + type WebSocketListener +} from '../../../src/ws'; +import { + BeatsSubscription, + type SubscriptionBeat2Response +} from '../../../src/thor/subscriptions'; + +describe('BlocksSubscription solo tests', () => { + let subscription: BeatsSubscription; + beforeEach(() => { + subscription = BeatsSubscription.at( + new MozillaWebSocketClient('ws://localhost:8669') + ); + }); + + test('data <- open', (done) => { + subscription + .addListener({ + onMessage: (message) => { + const data = message.data; + console.log(JSON.stringify(data, null, 2)); + done(); + }, + onOpen: () => { + console.log('WebSocket connection opened'); + }, + onClose: () => { + console.log(`WebSocket connection closed`); + }, + onError: (error) => { + console.error('WebSocket encountered an error:', error); + } + } satisfies WebSocketListener<SubscriptionBeat2Response>) + .open(); + }, 30000); + + afterEach(() => { + subscription.close(); + }); +}); diff --git a/packages/net/tests/thor/subscriptions/BlocksSubscription.solo.test.ts b/packages/net/tests/thor/subscriptions/BlocksSubscription.solo.test.ts new file mode 100644 index 000000000..da30ba968 --- /dev/null +++ b/packages/net/tests/thor/subscriptions/BlocksSubscription.solo.test.ts @@ -0,0 +1,39 @@ +import { afterEach, beforeEach, describe } from '@jest/globals'; +import { + MozillaWebSocketClient, + type WebSocketListener +} from '../../../src/ws'; +import { + BlocksSubscription, + type SubscriptionBlockResponse +} from '../../../src/thor/subscriptions'; + +describe('BlocksSubscription solo tests', () => { + let subscription: BlocksSubscription; + beforeEach(() => { + subscription = BlocksSubscription.at( + new MozillaWebSocketClient('ws://localhost:8669') + ); + }); + + test('data <- open', (done) => { + subscription + .addListener({ + onMessage: (message) => { + const data = message.data; + console.log(JSON.stringify(data, null, 2)); + done(); + }, + onOpen: () => {}, + onClose: () => {}, + onError: (error) => { + console.error('WebSocket error:', error); + } + } satisfies WebSocketListener<SubscriptionBlockResponse>) + .open(); + }, 30000); + + afterEach(() => { + subscription.close(); + }); +}); diff --git a/packages/net/tests/thor/subscriptions/NewTransactionSubscription.solo.test.ts b/packages/net/tests/thor/subscriptions/NewTransactionSubscription.solo.test.ts new file mode 100644 index 000000000..d70ef1978 --- /dev/null +++ b/packages/net/tests/thor/subscriptions/NewTransactionSubscription.solo.test.ts @@ -0,0 +1,35 @@ +import { afterEach, beforeEach, describe } from '@jest/globals'; +import { + MozillaWebSocketClient, + type WebSocketListener +} from '../../../src/ws'; +import { NewTransactionSubscription } from '../../../src/thor/subscriptions'; +import { type TXID } from '../../../src'; + +describe('NewTransactionSubscription solo tests', () => { + let subscription: NewTransactionSubscription; + beforeEach(() => { + subscription = NewTransactionSubscription.at( + new MozillaWebSocketClient('ws://localhost:8669') + ); + }); + + test('data <- open', (done) => { + subscription + .addListener({ + onMessage: (message) => { + const data = message.data; + console.log(JSON.stringify(data, null, 2)); + done(); + }, + onClose: () => {}, + onError: () => {}, + onOpen: () => {} + } satisfies WebSocketListener<TXID>) + .open(); + }, 30000); + + afterEach(() => { + subscription.close(); + }); +}); diff --git a/packages/net/tests/thor/transactions/EXP.solo.test.ts b/packages/net/tests/thor/transactions/EXP.solo.test.ts new file mode 100644 index 000000000..5e53a30d0 --- /dev/null +++ b/packages/net/tests/thor/transactions/EXP.solo.test.ts @@ -0,0 +1,205 @@ +import { describe, test } from '@jest/globals'; +import { + Address, + Clause, + HexUInt, + networkInfo, + Transaction, + type TransactionBody, + VET +} from '../../../../core/src'; +import { THOR_SOLO_URL, ThorClient } from '../../../../network/src'; + +import { secp256k1 as nc_secp256k1 } from '@noble/curves/secp256k1'; +import { Secp256k1 } from '@vechain/sdk-core'; + +describe('Solo Experiments', () => { + const thorClient = ThorClient.at(THOR_SOLO_URL + '/', { + isPollingEnabled: false + }); + const sender = { + privateKey: HexUInt.of( + 'ea5383ac1f9e625220039a4afac6a7f868bf1ad4f48ce3a1dd78bd214ee4ace5' + ), + address: Address.of('0x2669514f9fe96bc7301177ba774d3da8a06cace4') + }; + const receiver = { + address: Address.of('0x9e7911de289c3c856ce7f421034f66b6cde49c39') + }; + const gasPayer = { + privateKey: HexUInt.of( + '432f38bcf338c374523e83fdb2ebe1030aba63c7f1e81f7d76c5f53f4d42e766' + ), + address: Address.of('0x88b2551c3ed42ca663796c10ce68c88a65f73fe2') + }; + const OneVET = VET.of(1); + const clauses = [Clause.transferVET(receiver.address, OneVET)]; + + test('Delegated Tx', async () => { + const latestBlock = await thorClient.blocks.getBestBlockCompressed(); + console.log(latestBlock); + const gasToPay = await thorClient.gas.estimateGas( + clauses, + sender.address.toString() + ); + console.log(gasToPay); + const body: TransactionBody = { + chainTag: networkInfo.solo.chainTag, + blockRef: latestBlock?.id.slice(0, 18) ?? '0x0', + expiration: 0, + clauses, + gasPriceCoef: 0, + gas: gasToPay.totalGas, + dependsOn: null, + nonce: 2, + reserved: { + features: 1 // set the transaction to be delegated + } + }; + const tx = Transaction.of(body).signAsSenderAndGasPayer( + sender.privateKey.bytes, + gasPayer.privateKey.bytes + ); + console.log(tx.signature?.length); + const txResult = await thorClient.transactions.sendRawTransaction( + HexUInt.of(tx.encoded).toString() + ); + console.log(txResult); + const txReceipt = await thorClient.transactions.waitForTransaction( + tx.id.toString() + ); + console.log(txReceipt); + const txr = await thorClient.transactions.getTransaction( + tx.id.toString() + ); + console.log(txr); + }, 60000); + + test('NCC Tx', async () => { + const latestBlock = await thorClient.blocks.getBestBlockCompressed(); + const gasToPay = await thorClient.gas.estimateGas( + clauses, + sender.address.toString() + ); + const body: TransactionBody = { + chainTag: networkInfo.solo.chainTag, + blockRef: latestBlock?.id.slice(0, 18) ?? '0x0', + expiration: 0, + clauses, + gasPriceCoef: 0, + gas: gasToPay.totalGas, + dependsOn: null, + nonce: 1, + reserved: { + features: 1 // set the transaction to be delegated + } + }; + const tx = Transaction.of(body).signAsSenderAndGasPayer( + sender.privateKey.bytes, + gasPayer.privateKey.bytes + ); + // KEEP IT + // console.log(tx.signature?.length); + // const txResult = await thorClient.transactions.sendRawTransaction( + // HexUInt.of(tx.encoded).toString() + // ); + // console.log(txResult); + const aBody: TransactionBody = { + chainTag: networkInfo.solo.chainTag, + blockRef: latestBlock?.id.slice(0, 18) ?? '0x0', + expiration: 0, + clauses, + gasPriceCoef: 0, + gas: gasToPay.totalGas, + dependsOn: null, + nonce: 2, + reserved: { + features: 1 // set the transaction to be delegated + } + }; + const aTx = Transaction.of(aBody).signAsSender(sender.privateKey.bytes); + // KEEP IT + // const sig = nc_utils.concatBytes( + // aTx.signature as Uint8Array, + // (tx.signature as Uint8Array).slice(65) + // ); + const fTx = Transaction.of(aTx.body, tx.signature); + const fTxResult = await thorClient.transactions.sendRawTransaction( + HexUInt.of(fTx.encoded).toString() + ); + console.log(fTxResult); + }, 60000); + + test('verify', async () => { + const latestBlock = await thorClient.blocks.getBestBlockCompressed(); + const gasToPay = await thorClient.gas.estimateGas( + clauses, + sender.address.toString() + ); + // KEEP IT + // const senderPublicKey = Secp256k1.derivePublicKey( + // sender.privateKey.bytes, + // false + // ); + const gasPayerPublicKey = Secp256k1.derivePublicKey( + gasPayer.privateKey.bytes, + false + ); + const txA = Transaction.of({ + chainTag: networkInfo.solo.chainTag, + blockRef: latestBlock?.id.slice(0, 18) ?? '0x0', + expiration: 0, + clauses, + gasPriceCoef: 0, + gas: gasToPay.totalGas, + dependsOn: null, + nonce: 1, + reserved: { + features: 1 // set the transaction to be delegated + } + }); + const as = txA.signAsSender(sender.privateKey.bytes); + const ap = as.signAsGasPayer(sender.address, gasPayer.privateKey.bytes); + const sigmaA = nc_secp256k1.Signature.fromCompact( + ap.signature?.slice(-65).slice(0, 64) as Uint8Array + ); + const hashA = ap.getTransactionHash(sender.address).bytes; + const isVerifiedA = nc_secp256k1.verify( + sigmaA, + hashA, + gasPayerPublicKey + ); + console.log(isVerifiedA); + const txB = Transaction.of({ + chainTag: networkInfo.solo.chainTag, + blockRef: latestBlock?.id.slice(0, 18) ?? '0x0', + expiration: 0, + clauses, + gasPriceCoef: 0, + gas: gasToPay.totalGas, + dependsOn: null, + nonce: 2, + reserved: { + features: 1 // set the transaction to be delegated + } + }); + const bs = txB.signAsSender(sender.privateKey.bytes); + const bp = bs.signAsGasPayer(sender.address, gasPayer.privateKey.bytes); + const sigmaB = nc_secp256k1.Signature.fromCompact( + bp.signature?.slice(-65).slice(0, 64) as Uint8Array + ); + const hashB = bp.getTransactionHash(sender.address).bytes; + const isVerifiedB = nc_secp256k1.verify( + sigmaB, + hashB, + gasPayerPublicKey + ); + console.log(isVerifiedB); + const isVerifiedForge = nc_secp256k1.verify( + sigmaA, + hashB, + gasPayerPublicKey + ); + console.log(isVerifiedForge); + }); +}); diff --git a/packages/net/tests/thor/transactions/EXP.testnet.test.ts b/packages/net/tests/thor/transactions/EXP.testnet.test.ts new file mode 100644 index 000000000..c24ab6755 --- /dev/null +++ b/packages/net/tests/thor/transactions/EXP.testnet.test.ts @@ -0,0 +1,71 @@ +import { describe, test } from '@jest/globals'; +import { TESTNET_URL, ThorClient } from '../../../../network/src'; +import { + Address, + Clause, + HexUInt, + networkInfo, + Transaction, + type TransactionBody, + VET +} from '@vechain/sdk-core'; + +describe('Testnet Experiments', () => { + const thorClient = ThorClient.at(TESTNET_URL + '/', { + isPollingEnabled: false + }); + const sender = { + privateKey: HexUInt.of( + 'f9fc826b63a35413541d92d2bfb6661128cd5075fcdca583446d20c59994ba26' + ), + address: Address.of('0x7a28e7361fd10f4f058f9fefc77544349ecff5d6') + }; + const receiver = { + address: Address.of('0xb717b660cd51109334bd10b2c168986055f58c1a') + }; + const gasPayer = { + privateKey: HexUInt.of( + '521b7793c6eb27d137b617627c6b85d57c0aa303380e9ca4e30a30302fbc6676' + ), + address: Address.of('0x062F167A905C1484DE7e75B88EDC7439f82117DE') + }; + const OneVET = VET.of(1); + const clauses = [Clause.transferVET(receiver.address, OneVET)]; + + test('Delegated Tx', async () => { + const latestBlock = await thorClient.blocks.getBestBlockCompressed(); + console.log(latestBlock); + const gasToPay = await thorClient.transactions.estimateGas( + clauses, + gasPayer.address.toString() + ); + console.log(gasToPay); + const body: TransactionBody = { + chainTag: networkInfo.testnet.chainTag, + blockRef: latestBlock?.id.slice(0, 18) ?? '0x0', + expiration: 32, + clauses, + gasPriceCoef: 128, + gas: gasToPay.totalGas, + dependsOn: null, + // eslint-disable-next-line sonarjs/pseudo-random + nonce: Math.floor(1000000 * Math.random()), + reserved: { + features: 1 // set the transaction to be delegated + } + }; + const tx = Transaction.of(body).signAsSenderAndGasPayer( + sender.privateKey.bytes, + gasPayer.privateKey.bytes + ); + console.log('tx', tx); + const txResult = await thorClient.transactions.sendRawTransaction( + HexUInt.of(tx.encoded).toString() + ); + console.log(txResult); + const txReceipt = await thorClient.transactions.waitForTransaction( + tx.id.toString() + ); + console.log(txReceipt); + }, 60000); +}); diff --git a/packages/net/tests/thor/transactions/RetrieveRawTransactionByIID.testnet.test.ts b/packages/net/tests/thor/transactions/RetrieveRawTransactionByIID.testnet.test.ts new file mode 100644 index 000000000..7d60e24f2 --- /dev/null +++ b/packages/net/tests/thor/transactions/RetrieveRawTransactionByIID.testnet.test.ts @@ -0,0 +1,18 @@ +import { describe, test } from '@jest/globals'; +import { TxId } from '../../../../core'; +import { + FetchHttpClient, + RetrieveRawTransactionByID, + ThorNetworks +} from '../../../src'; + +describe('RetrieveRawTransactionByID testnet tests', () => { + test('ok <- askTo', async () => { + const txId = TxId.of( + '0xb6b5b47a5eee8b14e5222ac1bb957c0bbdc3d489850b033e3e544d9ca0cef934' + ); + const httpClient = FetchHttpClient.at(ThorNetworks.MAINNET); + const r = await RetrieveRawTransactionByID.of(txId).askTo(httpClient); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/transactions/RetrieveTransactionByIID.testnet.test.ts b/packages/net/tests/thor/transactions/RetrieveTransactionByIID.testnet.test.ts new file mode 100644 index 000000000..32f48194e --- /dev/null +++ b/packages/net/tests/thor/transactions/RetrieveTransactionByIID.testnet.test.ts @@ -0,0 +1,18 @@ +import { describe, test } from '@jest/globals'; +import { TxId } from '../../../../core'; +import { + FetchHttpClient, + RetrieveTransactionByID, + ThorNetworks +} from '../../../src'; + +describe('RetrieveTransactionByID testnet tests', () => { + test('ok <- askTo', async () => { + const txId = TxId.of( + '0xb6b5b47a5eee8b14e5222ac1bb957c0bbdc3d489850b033e3e544d9ca0cef934' + ); + const httpClient = FetchHttpClient.at(ThorNetworks.MAINNET); + const r = await RetrieveTransactionByID.of(txId).askTo(httpClient); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/transactions/RetrieveTransactionReceipt.testnet.test.ts b/packages/net/tests/thor/transactions/RetrieveTransactionReceipt.testnet.test.ts new file mode 100644 index 000000000..ae543125c --- /dev/null +++ b/packages/net/tests/thor/transactions/RetrieveTransactionReceipt.testnet.test.ts @@ -0,0 +1,18 @@ +import { describe, test } from '@jest/globals'; +import { TxId } from '../../../../core'; +import { + FetchHttpClient, + RetrieveTransactionReceipt, + ThorNetworks +} from '../../../src'; + +describe('RetrieveTransactionReceipt testnet tests', () => { + test('ok <- askTo', async () => { + const txId = TxId.of( + '0xb6b5b47a5eee8b14e5222ac1bb957c0bbdc3d489850b033e3e544d9ca0cef934' + ); + const httpClient = FetchHttpClient.at(ThorNetworks.MAINNET); + const r = await RetrieveTransactionReceipt.of(txId).askTo(httpClient); + console.log(JSON.stringify(r, null, 2)); + }); +}); diff --git a/packages/net/tests/thor/transactions/SendTransaction.solo.test.ts b/packages/net/tests/thor/transactions/SendTransaction.solo.test.ts new file mode 100644 index 000000000..da12b7efa --- /dev/null +++ b/packages/net/tests/thor/transactions/SendTransaction.solo.test.ts @@ -0,0 +1,34 @@ +import { describe, test } from '@jest/globals'; + +import { THOR_SOLO_URL, ThorClient } from '../../../../network/'; +import { + transfer1VTHOClause, + transferTransactionBody +} from '../../../../network/tests/thor-client/transactions/fixture'; +import { TEST_ACCOUNTS } from '../../../../network/tests/fixture'; +import { HexUInt, Transaction } from '@vechain/sdk-core'; +import { FetchHttpClient, SendTransaction } from '../../../src'; + +describe('SendTransaction solo tests', () => { + test('ok <- askTo', async () => { + const thorSoloClient = ThorClient.at(THOR_SOLO_URL); + const gasResult = await thorSoloClient.gas.estimateGas( + [transfer1VTHOClause], + TEST_ACCOUNTS.TRANSACTION.TRANSACTION_SENDER.address + ); + console.log(gasResult); + const tx = Transaction.of({ + ...transferTransactionBody, + gas: gasResult.totalGas, + nonce: 10000000 + }).sign( + HexUInt.of(TEST_ACCOUNTS.TRANSACTION.TRANSACTION_SENDER.privateKey) + .bytes + ).encoded; + console.log(tx); + const r = await SendTransaction.of(tx).askTo( + FetchHttpClient.at(THOR_SOLO_URL) + ); + console.log(r); + }); +}); diff --git a/packages/net/tests/ws/MozillaWebSocketClient.solo.test.ts b/packages/net/tests/ws/MozillaWebSocketClient.solo.test.ts new file mode 100644 index 000000000..820ba56b3 --- /dev/null +++ b/packages/net/tests/ws/MozillaWebSocketClient.solo.test.ts @@ -0,0 +1,25 @@ +import { afterEach, beforeEach, describe } from '@jest/globals'; +import { MozillaWebSocketClient } from '../../src/ws/MozillaWebSocketClient'; +import { type WebSocketListener } from '../../src/ws'; + +describe('MozillaWebSocketClient solo tests', () => { + let wsc: MozillaWebSocketClient; + beforeEach(() => { + wsc = new MozillaWebSocketClient('ws://localhost:8669'); + }); + + test('data <- open', (done) => { + wsc.addListener({ + onMessage: (message) => { + console.log(message.data); + done(); + } + } satisfies WebSocketListener<unknown>).open({ + path: '/subscriptions/beat2' + }); + }, 30000); + + afterEach(() => { + wsc.close(); + }); +}); diff --git a/packages/net/tsconfig.json b/packages/net/tsconfig.json new file mode 100644 index 000000000..b523f6a50 --- /dev/null +++ b/packages/net/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + }, + "include": [ + "./src/**/*.ts", + "./tests/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/net/typedoc.json b/packages/net/typedoc.json new file mode 100644 index 000000000..664b688e4 --- /dev/null +++ b/packages/net/typedoc.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": ["src/network.ts"] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f4b711ca9..f53d2d011 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3427,6 +3427,11 @@ dependencies: eslint-scope "5.1.1" +"@noble/ciphers@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.2.0.tgz#a7858e18eb620f6b2a327a7f0e647b6a78fd0727" + integrity sha512-YGdEUzYEd+82jeaVbSKKVp1jFZb8LwaNMIIzHFkihGvYdd/KKAr7KaJHdEdSYGredE3ssSravXIa0Jxg28Sv5w== + "@noble/ciphers@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.1.3.tgz#eb27085aa7ce94d8c6eaeb64299bab0589920ec1" @@ -3446,7 +3451,7 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/curves@1.7.0", "@noble/curves@^1.4.0", "@noble/curves@^1.6.0", "@noble/curves@^1.7.0", "@noble/curves@~1.7.0": +"@noble/curves@1.7.0", "@noble/curves@^1.6.0", "@noble/curves@^1.7.0", "@noble/curves@~1.7.0": version "1.7.0" resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== @@ -3850,7 +3855,7 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1" integrity sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== -"@scure/base@^1.2.1", "@scure/base@~1.2.1": +"@scure/base@^1.1.9", "@scure/base@^1.2.1", "@scure/base@~1.2.1": version "1.2.1" resolved "https://registry.npmjs.org/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== @@ -5363,6 +5368,37 @@ uuid "2.0.1" xmlhttprequest "1.8.0" +"@vechain/sdk-core@1.0.0-rc.6": + version "1.0.0-rc.6" + resolved "https://registry.yarnpkg.com/@vechain/sdk-core/-/sdk-core-1.0.0-rc.6.tgz#1c0b3c3297434f63d85bde65d344f8b34ae2a3ea" + integrity sha512-71ztrebtgi8u6uDu+HShnZt7OGqXa8tIyzv91GKjwXKDOSJKR6WVXfhWiMSCxEL4EBDAMsiRw5/zM+X4/8wxgg== + dependencies: + "@ethereumjs/rlp" "^5.0.2" + "@noble/ciphers" "^1.1.1" + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.5.0" + "@scure/base" "^1.1.9" + "@scure/bip32" "^1.4.0" + "@scure/bip39" "^1.4.0" + "@vechain/sdk-errors" "1.0.0-rc.6" + "@vechain/sdk-logging" "1.0.0-rc.6" + abitype "^1.0.6" + ethers "6.13.4" + fast-json-stable-stringify "^2.1.0" + viem "^2.21.45" + +"@vechain/sdk-errors@1.0.0-rc.6": + version "1.0.0-rc.6" + resolved "https://registry.yarnpkg.com/@vechain/sdk-errors/-/sdk-errors-1.0.0-rc.6.tgz#f0f0cb172ef0556a19c43aedd6cf9a4fa821cfd0" + integrity sha512-DzIbaXOis8/yEfRc1KED8Kyo89K3jTJ+2T+gKvAYDyeut0cR4pB8l64ufYagqFDBkU0hJkOQsoz0bF8CRnZX8w== + +"@vechain/sdk-logging@1.0.0-rc.6": + version "1.0.0-rc.6" + resolved "https://registry.yarnpkg.com/@vechain/sdk-logging/-/sdk-logging-1.0.0-rc.6.tgz#f6ca4d37c4eb7aab848c1c973b00d6e6ba103668" + integrity sha512-RaPvzZM9ez2jsEowW6/3UXU+fRYiu7KPhiDf3aD19JWpMN92jU9T1NpX4aNsphH+XjhVCj7Ucz4MVwK+TwhGiw== + dependencies: + "@vechain/sdk-errors" "1.0.0-rc.6" + "@vechain/vebetterdao-contracts@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@vechain/vebetterdao-contracts/-/vebetterdao-contracts-4.1.0.tgz#5a081bf9c548ea777fe16dcab40536d6d2cc1a62" @@ -8377,6 +8413,19 @@ ethereumjs-util@^7.1.4: ethereum-cryptography "^0.1.3" rlp "^2.2.4" +ethers@6.13.4: + version "6.13.4" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.4.tgz#bd3e1c3dc1e7dc8ce10f9ffb4ee40967a651b53c" + integrity sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA== + dependencies: + "@adraffy/ens-normalize" "1.10.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "22.7.5" + aes-js "4.0.0-beta.5" + tslib "2.7.0" + ws "8.17.1" + ethers@6.13.5, ethers@^6.9.0: version "6.13.5" resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.5.tgz#8c1d6ac988ac08abc3c1d8fabbd4b8b602851ac4" @@ -11559,10 +11608,10 @@ outvariant@^1.4.0, outvariant@^1.4.3: resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873" integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA== -ox@0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/ox/-/ox-0.6.0.tgz#ba8f89d68b5e8afe717c8a947ffacc0669ea155d" - integrity sha512-blUzTLidvUlshv0O02CnLFqBLidNzPoAZdIth894avUAotTuWziznv6IENv5idRuOSSP3dH8WzcYw84zVdu0Aw== +ox@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.6.5.tgz#e6506a589bd6af9b5fecfcb2c641b63c9882edb6" + integrity sha512-vmnH8KvMDwFZDbNY1mq2CBRBWIgSliZB/dFV0xKp+DfF/dJkTENt6nmA+DzHSSAgL/GO2ydjkXWvlndJgSY4KQ== dependencies: "@adraffy/ens-normalize" "^1.10.1" "@noble/curves" "^1.6.0" @@ -14391,10 +14440,10 @@ vfile@^6.0.0: "@types/unist" "^3.0.0" vfile-message "^4.0.0" -viem@^2.22.8: - version "2.22.8" - resolved "https://registry.npmjs.org/viem/-/viem-2.22.8.tgz#fbc51215133066b89730e9a2aa2c7c0cb975a8e8" - integrity sha512-iB3PW/a/qzpYbpjo3R662u6a/zo6piZHez/N/bOC25C79FYXBCs8mQDqwiHk3FYErUhS4KVZLabKV9zGMd+EgQ== +viem@^2.21.45, viem@^2.22.8: + version "2.22.9" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.22.9.tgz#14ddb7f1ccf900784e347e1aa157e8e58043b0c2" + integrity sha512-2yy46qYhcdo8GZggQ3Zoq9QCahI0goddzpVI/vSnTpcClQBSDxYRCuAqRzzLqjvJ7hS0UYgplC7eRkM2sYgflw== dependencies: "@noble/curves" "1.7.0" "@noble/hashes" "1.6.1" @@ -14402,8 +14451,7 @@ viem@^2.22.8: "@scure/bip39" "1.5.0" abitype "1.0.7" isows "1.0.6" - ox "0.6.0" - webauthn-p256 "0.0.10" + ox "0.6.5" ws "8.18.0" vite-node@2.1.4: @@ -14547,14 +14595,6 @@ web3-utils@^1.3.6: randombytes "^2.1.0" utf8 "3.0.0" -webauthn-p256@0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.10.tgz#877e75abe8348d3e14485932968edf3325fd2fdd" - integrity sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA== - dependencies: - "@noble/curves" "^1.4.0" - "@noble/hashes" "^1.4.0" - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"