From 5bf2f8fa3d21f791d46eab0ad84ab04471ac3f15 Mon Sep 17 00:00:00 2001 From: Bora Date: Mon, 17 Mar 2025 13:58:18 +0300 Subject: [PATCH] feat: add rosettanet support --- package-lock.json | 7 + package.json | 1 + src/global/constants.ts | 12 + src/index.ts | 1 + src/types/api/rosettaRpc/components.ts | 459 +++++++++++++++++++++++++ src/types/api/rosettaRpc/index.ts | 1 + src/wallet/index.ts | 1 + src/wallet/rosettanetAccount.ts | 353 +++++++++++++++++++ src/wallet/rosettanetConnect.ts | 200 +++++++++++ 9 files changed, 1035 insertions(+) create mode 100644 src/types/api/rosettaRpc/components.ts create mode 100644 src/types/api/rosettaRpc/index.ts create mode 100644 src/wallet/rosettanetAccount.ts create mode 100644 src/wallet/rosettanetConnect.ts diff --git a/package-lock.json b/package-lock.json index eebb9d382..b3a3380ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "isomorphic-fetch": "~3.0.0", "lossless-json": "^4.0.1", "pako": "^2.0.4", + "rosettanet": "^1.2.2", "starknet-types-07": "npm:@starknet-io/types-js@^0.7.10", "ts-mixer": "^6.0.3" }, @@ -16917,6 +16918,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rosettanet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rosettanet/-/rosettanet-1.2.2.tgz", + "integrity": "sha512-htYM0yRxN16S4lNh6u20i9denuNzhxjr5wgJJSzaNoGgkd01Bf83SLGLl+E78Vfs5gdao/zfUPj3MAeN73e9IA==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index 8172a4c61..d6891ae29 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "isomorphic-fetch": "~3.0.0", "lossless-json": "^4.0.1", "pako": "^2.0.4", + "rosettanet": "^1.2.2", "starknet-types-07": "npm:@starknet-io/types-js@^0.7.10", "ts-mixer": "^6.0.3" }, diff --git a/src/global/constants.ts b/src/global/constants.ts index 7a2a2a7af..b1f74b028 100644 --- a/src/global/constants.ts +++ b/src/global/constants.ts @@ -46,6 +46,14 @@ export enum StarknetChainId { SN_SEPOLIA = '0x534e5f5345504f4c4941', // encodeShortString('SN_SEPOLIA') } +export enum RosettaNetworkName { + MAIN = 'MAIN', +} + +export enum RosettanetChainId { + MAIN = '0x52535453', +} + export enum TransactionHashPrefix { DECLARE = '0x6465636c617265', // encodeShortString('declare'), DEPLOY = '0x6465706c6f79', // encodeShortString('deploy'), @@ -78,6 +86,10 @@ export const RPC_NODES = { ], } as const; +export const ROSETTANET_RPC_NODES = { + MAIN: ['https://alpha-deployment.rosettanet.io'], +}; + export const OutsideExecutionCallerAny = '0x414e595f43414c4c4552'; // encodeShortString('ANY_CALLER') export const SNIP9_V1_INTERFACE_ID = '0x68cfd18b92d1907b8ba3cc324900277f5a3622099431ea85dd8089255e4181'; diff --git a/src/index.ts b/src/index.ts index 659a79b86..2dd536386 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,7 @@ export * from './utils/contract'; export * from './utils/transactionReceipt'; export * from './utils/units'; export * as wallet from './wallet/connect'; +export * as rosettanetWallet from './wallet/rosettanetConnect'; export * from './global/config'; export * from './global/logger'; export * from './global/logger.type'; diff --git a/src/types/api/rosettaRpc/components.ts b/src/types/api/rosettaRpc/components.ts new file mode 100644 index 000000000..3106cd672 --- /dev/null +++ b/src/types/api/rosettaRpc/components.ts @@ -0,0 +1,459 @@ +/** + * PRIMITIVES + */ + +/** + * An Ethereum address, represented as 40 hex digits. + * @pattern ^0x[a-fA-F0-9]{40}$ + */ +export type ETH_ADDRESS = string; + +/** + * A transaction hash, represented as a 32-byte hex string. + * @pattern ^0x[a-fA-F0-9]{64}$ + */ +export type TXN_HASH = string; + +/** + * A block hash, represented as a 32-byte hex string. + * @pattern ^0x[a-fA-F0-9]{64}$ + */ +export type BLOCK_HASH = string; + +/** + * A block number, represented as a positive integer. + */ +export type BLOCK_NUMBER = string | number | 'latest' | 'finalized' | 'pending'; + +/** + * A nonce, represented as a hex string of at most 16 hex digits. + * @pattern ^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,15})$ + */ +export type NONCE = string; + +/** + * Gas value, represented as a hex string. + * @pattern ^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,15})$ + */ +export type GAS = string; + +/** + * A storage key, represented as a 32-byte hex string. + * @pattern ^0x[a-fA-F0-9]{64}$ + */ +export type STORAGE_KEY = string; + +/** + * A 256-bit integer, represented as a hex string of length at most 64. + * @pattern ^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,63})$ + */ +export type UINT256 = string; + +/** + * A 128-bit integer, represented as a hex string of length at most 32. + * @pattern ^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,31})$ + */ +export type UINT128 = string; + +/** + * A signature, represented as an array of two 32-byte hex strings (r, s). + */ +export type SIGNATURE = [string, string]; + +/** + * A chain ID, represented as a hex string. + * @pattern ^0x[a-fA-F0-9]{1,64}$ + */ +export type CHAIN_ID = string; + +/** + * Transaction Status Enum + */ +export enum TX_STATUS { + /** The transaction has been created but not yet broadcasted. */ + CREATED = 'CREATED', + + /** The transaction has been received by the network but not yet included in a block. */ + PENDING = 'PENDING', + + /** The transaction is included in a block but is not yet finalized. */ + INCLUDED = 'INCLUDED', + + /** The transaction has been successfully executed and confirmed on-chain. */ + SUCCESS = 'SUCCESS', + + /** The transaction was included in a block but reverted due to an error. */ + FAILED = 'FAILED', + + /** The transaction was dropped from the mempool and will not be processed. */ + DROPPED = 'DROPPED', + + /** The transaction was replaced by another transaction with the same nonce but a higher gas price. */ + REPLACED = 'REPLACED', +} + +/** + * Call that modifies state and consumes gas. + */ +export type STATE_CHANGING_CALL = 'CALL' | 'CREATE' | 'DELEGATECALL' | 'STATICCALL'; + +/** + * Call that does not modify state and is free to execute. + */ +export type VIEW_CALL = 'STATICCALL'; + +/** + * Calls that occur internally between smart contracts. + */ +export type INTERNAL_CALL = 'CALL' | 'DELEGATECALL' | 'STATICCALL'; + +/** + * Low-level Ethereum calls. + */ +export type LOW_LEVEL_CALL = 'CALLCODE' | 'SELFDESTRUCT'; + +/** + * ETH-specific calls. + */ +export type ETH_CALL = 'TRANSFER' | 'SEND'; + +export type CALL_TYPE = STATE_CHANGING_CALL | VIEW_CALL | INTERNAL_CALL | LOW_LEVEL_CALL | ETH_CALL; + +/** + * Block Status Enum + */ +export enum BLOCK_STATUS { + /** The block has been proposed but not yet finalized. */ + PENDING = 'PENDING', + + /** The block has been included in the chain but is not yet finalized. */ + INCLUDED = 'INCLUDED', + + /** The block has been confirmed and is unlikely to be reverted. */ + CONFIRMED = 'CONFIRMED', + + /** The block has been finalized and cannot be changed. */ + FINALIZED = 'FINALIZED', + + /** The block was reorged out of the chain and is no longer valid. */ + REORGED = 'REORGED', + + /** The block was invalidated due to a protocol or consensus issue. */ + INVALID = 'INVALID', +} + +/** + * Block Tag of 'latest' or 'pending' block. + */ +export type BLOCK_TAG = 'latest' | 'pending'; + +/** + * Transaction Type Enum + */ +export enum TX_TYPE { + /** A standard ETH transfer from one EOA to another. */ + TRANSFER = 'TRANSFER', + + /** A transaction that deploys a new smart contract. */ + CONTRACT_CREATION = 'CONTRACT_CREATION', + + /** A transaction calling a smart contract function, possibly modifying state. */ + CONTRACT_CALL = 'CONTRACT_CALL', + + /** A transaction that delegates execution to another contract while maintaining the caller's context. */ + DELEGATECALL = 'DELEGATECALL', + + /** A read-only transaction that does not modify state (gas-free). */ + STATICCALL = 'STATICCALL', + + /** A transaction using the EIP-1559 fee structure (Base Fee + Priority Fee). */ + EIP1559 = 'EIP1559', + + /** A legacy transaction using a gas price instead of the EIP-1559 model. */ + LEGACY = 'LEGACY', + + /** A transaction that cancels or replaces a pending transaction with a higher gas price. */ + CANCEL = 'CANCEL', + + /** A transaction that self-destructs a contract and transfers remaining ETH. */ + SELFDESTRUCT = 'SELFDESTRUCT', +} + +/** + * Ethereum Transaction Type (Based on provided data) + */ +export type TX = { + /** List of addresses involved in the transaction and their access lists (if any). */ + accessList: Array<{ + address: ETH_ADDRESS; + storageKeys: string[]; + }>; + + /** Block hash where the transaction was included. */ + blockHash: BLOCK_HASH; + + /** Block number in which the transaction was included (hex string). */ + blockNumber: BLOCK_NUMBER; + + /** Chain ID (for preventing replay attacks). */ + chainId: CHAIN_ID; + + /** Sender's Ethereum address. */ + from: ETH_ADDRESS; + + /** Gas limit for the transaction (hex string). */ + gas: string; + + /** Gas price (used for legacy transactions). */ + gasPrice: string; + + /** Transaction hash (unique identifier). */ + hash: TXN_HASH; + + /** Input data for the transaction (used in contract calls). */ + input: string; + + /** Maximum fee per gas (used for EIP-1559 transactions). */ + maxFeePerGas: string; + + /** Maximum priority fee per gas (used for EIP-1559 transactions). */ + maxPriorityFeePerGas: string; + + /** Nonce of the transaction (unique per sender). */ + nonce: string; + + /** Transaction signature component 'r'. */ + r: string; + + /** Transaction signature component 's'. */ + s: string; + + /** Transaction recipient address (or `null` for contract creation). */ + to: ETH_ADDRESS | null; + + /** Index of the transaction in the block. */ + transactionIndex: number; + + /** Type of transaction (EIP-1559 or legacy). */ + type: string; + + /** Transaction signature component 'v'. */ + v: string; + + /** Value of Ether sent in the transaction (in Wei, hex string). */ + value: string; + + /** Parity for the transaction's signature (optional, included in some cases). */ + yParity: string; +}; + +/** + * Ethereum Transaction Receipt Type + */ +export type TX_RECEIPT = { + /** Transaction hash (32 bytes). */ + transactionHash: TXN_HASH; + + /** Index of the transaction within the block. */ + transactionIndex: number; + + /** Block number in which the transaction was included. */ + blockNumber: BLOCK_NUMBER; + + /** Block hash in which the transaction was included. */ + blockHash: BLOCK_HASH; + + /** The address that deployed the contract (if applicable). */ + contractAddress: ETH_ADDRESS | null; + + /** Gas used by the transaction execution. */ + gasUsed: string; + + /** Cumulative gas used by the transaction and all preceding ones in the block. */ + cumulativeGasUsed: string; + + /** Effective gas price (EIP-1559) or gas price (legacy). */ + effectiveGasPrice: string; + + /** Logs generated by the transaction. */ + logs: LOG_ENTRY[]; + + /** Logs bloom filter for indexing events. */ + logsBloom: string; + + /** Transaction status: `1` for success, `0` for failure. */ + status: TX_STATUS; + + /** Type of transaction (EIP-1559, Legacy, etc.). */ + type: TX_TYPE; + + /** The address of the sender of the transaction. */ + from: ETH_ADDRESS; + + /** The address of the recipient or `null` if contract creation. */ + to: ETH_ADDRESS | null; +}; + +/** + * Ethereum Log Entry Type + */ +export type LOG_ENTRY = { + /** Address that emitted the log. */ + address: ETH_ADDRESS; + + /** List of indexed topics (max 4). */ + topics: string[]; + + /** Non-indexed data field. */ + data: string; + + /** Block number in which the log was included. */ + blockNumber: BLOCK_NUMBER; + + /** Transaction hash associated with the log. */ + transactionHash: TXN_HASH; + + /** Index of the log within the transaction. */ + logIndex: number; + + /** Index of the log within the block. */ + blockIndex: number; +}; + +/** + * Ethereum Block Type + */ +export type BLOCK = { + /** Block number (or `null` if it's a pending block). */ + number: BLOCK_NUMBER | null; + + /** Block hash (or `null` if it's a pending block). */ + hash: BLOCK_HASH | null; + + /** Hash of the parent block. */ + parentHash: BLOCK_HASH; + + /** Hash of the state root after executing this block. */ + stateRoot: string; + + /** Hash of the transactions root. */ + transactionsRoot: string; + + /** Hash of the receipts root. */ + receiptsRoot: string; + + /** Bloom filter for logs in the block. */ + logsBloom: string; + + /** Address of the block miner. */ + miner: ETH_ADDRESS; + + /** Difficulty of the block. */ + difficulty: string; + + /** Total difficulty up to this block. */ + totalDifficulty: string; + + /** Extra data included in the block. */ + extraData: string; + + /** Size of the block in bytes. */ + size: string; + + /** Gas limit for the block. */ + gasLimit: string; + + /** Total gas used in this block. */ + gasUsed: string; + + /** Base fee per gas (EIP-1559) or `null` for pre-London blocks. */ + baseFeePerGas?: string | null; + + /** Timestamp when the block was mined. */ + timestamp: number; + + /** List of transaction hashes included in the block. */ + transactions: TXN_HASH[]; + + /** List of uncle block hashes (empty for modern Ethereum blocks). */ + uncles: BLOCK_HASH[]; + + /** Mix hash used in proof-of-work (pre-Merge). */ + mixHash?: string; + + /** Proof-of-work nonce (pre-Merge). */ + nonce?: string; +}; + +/** + * Ethereum Block Receipts Type + */ +export type BLOCK_RECEIPTS = { + /** The block number in which these receipts belong. */ + blockNumber: BLOCK_NUMBER; + + /** The block hash in which these receipts belong. */ + blockHash: BLOCK_HASH; + + /** List of transaction receipts in the block. */ + receipts: TX_RECEIPT[]; +}; + +/** + * Ethereum Transaction Request Type + */ +export type TX_REQUEST = { + /** The transaction type (optional). */ + type?: null | number; + + /** The target address of the transaction (optional). */ + to?: null | ETH_ADDRESS; + + /** The sender address of the transaction (optional). */ + from?: null | ETH_ADDRESS; + + /** The nonce of the transaction (optional). */ + nonce?: null | number; + + /** The gas limit for the transaction (optional). */ + gasLimit?: null | string; + + /** The gas price for legacy transactions (optional). */ + gasPrice?: null | string; + + /** The max priority fee to pay per gas (EIP-1559) (optional). */ + maxPriorityFeePerGas?: null | string; + + /** The max total fee to pay per gas (EIP-1559) (optional). */ + maxFeePerGas?: null | string; + + /** The transaction data (optional). */ + data?: null | string; + + /** The value of the transaction in wei (optional). */ + value?: null | string; + + /** The chain ID for the transaction (optional). */ + chainId?: null | string; + + /** The access list for EIP-2930 transactions (optional). */ + accessList?: null | Array<{ + address: ETH_ADDRESS; + storageKeys: string[]; + }>; + + /** Custom data for network-specific values (optional). */ + customData?: any; + + /** Block tag for call or estimateGas (optional). */ + blockTag?: string; + + /** Whether CCIP-read should be enabled (optional). */ + enableCcipRead?: boolean; + + /** Blob versioned hashes for EIP-4844 transactions (optional). */ + blobVersionedHashes?: null | Array; + + /** The maximum fee per blob gas for EIP-4844 (optional). */ + maxFeePerBlobGas?: null | string; +}; diff --git a/src/types/api/rosettaRpc/index.ts b/src/types/api/rosettaRpc/index.ts new file mode 100644 index 000000000..aba54cf29 --- /dev/null +++ b/src/types/api/rosettaRpc/index.ts @@ -0,0 +1 @@ +export * as COMPONENTS from './components'; diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 362a768e5..d5b56d936 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -1 +1,2 @@ export * from './account'; +export * from './rosettanetAccount'; diff --git a/src/wallet/rosettanetAccount.ts b/src/wallet/rosettanetAccount.ts new file mode 100644 index 000000000..22b8c6f91 --- /dev/null +++ b/src/wallet/rosettanetAccount.ts @@ -0,0 +1,353 @@ +import { WatchAssetParameters, TypedData } from 'starknet-types-07'; +import { prepareMulticallCalldata } from 'rosettanet'; +import { Account, AccountInterface } from '../account'; +import { + EthereumWindowObject, + requestAccounts, + watchAsset, + sendTransaction, + signMessage, + requestChainId, + switchRosettanetChain, + getPermissions, + personalSign, + accounts, + getBlockNumber, + call, + estimateGas, + gasPrice, + getBalance, + getBlockByHash, + getBlockByNumber, + getBlockTransactionCountByHash, + getBlockTransactionCountByNumber, + getCode, + getTransactionHashByBlockHashAndIndex, + getTransactionHashByBlockNumberAndIndex, + getTransactionByHash, + getTransactionCount, + getTransactionReceipt, +} from './rosettanetConnect'; +import { + CairoVersion, + ProviderOptions, + AllowArray, + Call, + Uint256, + ArraySignatureType, +} from '../types'; +import { ProviderInterface } from '../provider'; +import { + BLOCK_HASH, + BLOCK_NUMBER, + BLOCK_TAG, + TX_REQUEST, + TXN_HASH, +} from '../types/api/rosettaRpc/components'; +import { RosettanetChainId } from '../global/constants'; +import { addHexPrefix, removeHexPrefix } from '../utils/encode'; +import { cairo } from '../utils/calldata'; +import { toHex } from '../utils/num'; + +export class RosettanetAccount extends Account implements AccountInterface { + public walletProvider: EthereumWindowObject; + + constructor( + providerOrOptions: ProviderOptions | ProviderInterface, + walletProvider: EthereumWindowObject, + cairoVersion?: CairoVersion, + address: string = '' + ) { + super(providerOrOptions, address, '', cairoVersion); // At this point unknown address + this.walletProvider = walletProvider; + + if (!address.length) { + // eslint-disable-next-line no-console + console.warn( + '@deprecated Use static method WalletAccount.connect or WalletAccount.connectSilent instead. Constructor {@link WalletAccount.(format:2)}.' + ); + requestAccounts(this.walletProvider).then(([accountAddress]) => { + this.address = accountAddress.toLowerCase(); + }); + } + } + + /** + * Send transaction to the wallet. + * @param params Ethereum transaction object. + * @returns Transaction hash. + */ + public sendTransactionRosettanet(params: TX_REQUEST) { + return sendTransaction(this.walletProvider, params); + } + + /** + * Request the current chain ID from the wallet. + * @returns The current Wallet Chain ID. + */ + public chainIdRosettanet() { + return requestChainId(this.walletProvider); + } + + /** + * Sign typed data using the wallet. Uses personal_sign method. + * @param message The typed data to sign. + * @param address The wallet address to sign. + * @returns Signatures as strings. + */ + public personalSignRosettanet(message: string, address: string) { + return personalSign(this.walletProvider, message, address); + } + + /** + * Request connected accounts + * @returns connected accounts addresses + */ + public accountsRosettanet() { + return accounts(this.walletProvider); + } + + /** + * Request latest block number in Starknet + * @returns latest block number in hexadecimal format + */ + public blockNumberRosettanet() { + return getBlockNumber(this.walletProvider); + } + + /** + * Call request. + * @param tx Ethereum transaction object. + * @returns Answer from called contract. + */ + public callRosettanet(tx: TX_REQUEST) { + return call(this.walletProvider, tx); + } + + /** + * Estimated gas fee for the transaction. + * @param tx Ethereum transaction object. + * @returns Estimated gas amount. + */ + public estimateGasRosettanet(tx: TX_REQUEST) { + return estimateGas(this.walletProvider, tx); + } + + /** + * Latest gas price in network. + * @returns Latest gas price. + */ + public gasPriceRosettanet() { + return gasPrice(this.walletProvider); + } + + /** + * STRK balance of given address. + * @param address Address to check balance. + * @param block Block number or hash. (optional) + * @returns STRK balance. + */ + public getBalanceRosettanet( + address: string, + block: BLOCK_HASH | BLOCK_NUMBER | BLOCK_TAG = 'latest' + ) { + return getBalance(this.walletProvider, address, block); + } + + /** + * Block by given block hash. + * @param blockHash Block hash. + * @param hydratedTx Hydrated transactions (optional) + * @returns Block by given block hash. + */ + public getBlockByHashRosettanet(blockHash: BLOCK_HASH, hydratedTx: boolean = false) { + return getBlockByHash(this.walletProvider, blockHash, hydratedTx); + } + + /** + * Block by given block number. + * @param blockNumber Block number or block tag. + * @param hydratedTx Hydrated transactions (optional) + * @returns Block by given block number. + */ + public getBlockByNumberRosettanet( + blockNumber: BLOCK_NUMBER | BLOCK_TAG, + hydratedTx: boolean = false + ) { + return getBlockByNumber(this.walletProvider, blockNumber, hydratedTx); + } + + /** + * Transaction count of given block hash. + * @param blockHash Block hash. + * @returns Transaction count of given block hash. + */ + public getBlockTransactionCountByHashRosettanet(blockHash: BLOCK_HASH) { + return getBlockTransactionCountByHash(this.walletProvider, blockHash); + } + + /** + * Transaction count of given block number. + * @param blockNumber Block number or block tag.. + * @returns Transaction count of given block number. + */ + public getBlockTransactionCountByNumberRosettanet(blockNumber: BLOCK_NUMBER | BLOCK_TAG) { + return getBlockTransactionCountByNumber(this.walletProvider, blockNumber); + } + + public getCodeRosettanet( + address: string, + block: BLOCK_HASH | BLOCK_NUMBER | BLOCK_TAG = 'latest' + ) { + return getCode(this.walletProvider, address, block); + } + + public getTransactionHashByBlockHashAndIndexRosettanet(blockHash: BLOCK_HASH, index: string) { + return getTransactionHashByBlockHashAndIndex(this.walletProvider, blockHash, index); + } + + public getTransactionHashByBlockNumberAndIndexRosettanet( + blockNumber: BLOCK_NUMBER | BLOCK_TAG, + index: string + ) { + return getTransactionHashByBlockNumberAndIndex(this.walletProvider, blockNumber, index); + } + + public getTransactionByHashRosettanet(txHash: TXN_HASH) { + return getTransactionByHash(this.walletProvider, txHash); + } + + /** + * Transaction count of given address. + * @param address address. + * @returns Transaction count of given address. + */ + public getTransactionCountRosettanet( + address: string, + block: BLOCK_HASH | BLOCK_NUMBER | BLOCK_TAG = 'latest' + ) { + return getTransactionCount(this.walletProvider, address, block); + } + + /** + * Transaction receipt of given transaction hash. + * @param txHash address. + * @returns Transaction receipt of given transaction hash. + */ + public getTransactionReceiptRosettanet(txHash: TXN_HASH) { + return getTransactionReceipt(this.walletProvider, txHash); + } + + // WALLET ACCOUNT METHODS + + public requestAccounts() { + return requestAccounts(this.walletProvider); + } + + /** + * Request Permission for wallet account + * @returns allowed accounts addresses + */ + public getPermissions() { + if (this.walletProvider.name === 'Coinbase Wallet') { + throw new Error('Get permissions Method not found in Coinbase Wallet'); + } + return getPermissions(this.walletProvider); + } + + public switchStarknetChain(chainId: RosettanetChainId) { + return switchRosettanetChain(this.walletProvider, chainId); + } + + /** + * Request adding ERC20 Token to Wallet List + * @param asset WatchAssetParameters + * @returns boolean + */ + public watchAsset(asset: WatchAssetParameters) { + return watchAsset(this.walletProvider, asset); + } + + override declare(): Promise<{ transaction_hash: string; class_hash: string }> { + throw new Error('Declare Method not implemented in Rosettanet Account Class.'); + } + + override deploy(): Promise<{ transaction_hash: string; contract_address: string[] }> { + throw new Error('Deploy Method not implemented in Rosettanet Account Class.'); + } + + /** + * Sign typed data using the wallet. Uses eth_signTypedData_v4 method. + * @param message The typed data to sign. + * @returns Signature as strings. + */ + override async signMessage(message: TypedData) { + const evmSignedHash = await signMessage(this.walletProvider, message, this.address); + + if (!evmSignedHash || (evmSignedHash.length !== 132 && evmSignedHash.length !== 130)) { + throw new Error('Ethereum Signature error'); + } + + const signedHashWithoutPrefix = removeHexPrefix(evmSignedHash); + const r: Uint256 = cairo.uint256(addHexPrefix(signedHashWithoutPrefix.slice(0, 63))); // First 64 chars → r (32 bytes) + const s: Uint256 = cairo.uint256(addHexPrefix(signedHashWithoutPrefix.slice(64, 127))); // Next 64 chars → s (32 bytes) + const v = addHexPrefix(signedHashWithoutPrefix.slice(128, 130)); // Last 2 chars → v (1 byte) + + if (v !== '0x1c' && v !== '0x1b') { + throw new Error('Invalid Ethereum Signature'); + } + + return [ + toHex(r.low), + toHex(r.high), + toHex(s.low), + toHex(s.high), + toHex(v), + ] as ArraySignatureType; + } + + override async execute(calls: AllowArray): Promise<{ transaction_hash: string }> { + const txCalls = [].concat(calls as any).map((it) => { + const { contractAddress, entrypoint, calldata } = it; + return { + contract_address: contractAddress, + entry_point: entrypoint, + calldata, + }; + }); + + const params = { + calls: txCalls, + }; + + const txData = prepareMulticallCalldata(params.calls); + + const txObject = { + from: this.address, + to: this.address, + data: txData, + value: '0x0', + }; + + const txHash = await sendTransaction(this.walletProvider, txObject); + return { transaction_hash: txHash }; + } + + static async connect( + provider: ProviderInterface, + walletProvider: EthereumWindowObject, + cairoVersion?: CairoVersion + ) { + const [accountAddress] = await requestAccounts(walletProvider); + return new RosettanetAccount(provider, walletProvider, cairoVersion, accountAddress); + } + + static async connectSilent( + provider: ProviderInterface, + walletProvider: EthereumWindowObject, + cairoVersion?: CairoVersion + ) { + const [accountAddress] = await requestAccounts(walletProvider); + return new RosettanetAccount(provider, walletProvider, cairoVersion, accountAddress); + } +} diff --git a/src/wallet/rosettanetConnect.ts b/src/wallet/rosettanetConnect.ts new file mode 100644 index 000000000..582348e2a --- /dev/null +++ b/src/wallet/rosettanetConnect.ts @@ -0,0 +1,200 @@ +import { + TypedData, + WatchAssetParameters, + RequestFn, + WalletEventListener, + Permission, + ChainId, +} from 'starknet-types-07'; +import { + BLOCK_HASH, + BLOCK_NUMBER, + BLOCK_TAG, + TX_REQUEST, + TXN_HASH, +} from '../types/api/rosettaRpc/components'; + +interface Request extends RequestFn { + (request: { method: string; params?: Array }): Promise; +} + +export interface EthereumWindowObject { + request: Request; + on: WalletEventListener; + off: WalletEventListener; + id: string; + name: string; + icon: string; + version: '1.0.0'; +} + +export function requestAccounts(ewo: EthereumWindowObject): Promise { + return ewo.request({ method: 'eth_requestAccounts' }); +} + +export function watchAsset( + ewo: EthereumWindowObject, + asset: WatchAssetParameters +): Promise { + return ewo.request({ method: 'wallet_watchAsset', params: [asset] }); +} + +export function requestChainId(ewo: EthereumWindowObject): Promise { + return ewo.request({ method: 'eth_chainId' }); +} + +export function sendTransaction(ewo: EthereumWindowObject, tx: TX_REQUEST): Promise { + return ewo.request({ method: 'eth_sendTransaction', params: [tx] }); +} + +export function switchRosettanetChain( + ewo: EthereumWindowObject, + chainId: ChainId +): Promise { + return ewo.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId }], + }); +} + +export function getPermissions(ewo: EthereumWindowObject): Promise { + return ewo.request({ method: 'wallet_getPermissions' }); +} + +export function personalSign( + ewo: EthereumWindowObject, + message: string, + address: string +): Promise { + return ewo.request({ method: 'personal_sign', params: [message, address] }); +} + +export function accounts(ewo: EthereumWindowObject): Promise { + return ewo.request({ method: 'eth_accounts' }); +} + +export function clientVersion(ewo: EthereumWindowObject): Promise { + return ewo.request({ method: 'web3_clientVersion' }); +} + +export function getBlockNumber(ewo: EthereumWindowObject): Promise { + return ewo.request({ method: 'eth_blockNumber' }); +} + +export function call(ewo: EthereumWindowObject, tx: TX_REQUEST): Promise { + return ewo.request({ method: 'eth_call', params: [tx] }); +} + +export function estimateGas(ewo: EthereumWindowObject, tx: TX_REQUEST): Promise { + return ewo.request({ method: 'eth_estimateGas', params: [tx] }); +} + +export function gasPrice(ewo: EthereumWindowObject): Promise { + return ewo.request({ method: 'eth_gasPrice' }); +} + +export function getBalance( + ewo: EthereumWindowObject, + address: string, + block: BLOCK_HASH | BLOCK_NUMBER | BLOCK_TAG = 'latest' +): Promise { + return ewo.request({ method: 'eth_getBalance', params: [address, block] }); +} + +export function getBlockByHash( + ewo: EthereumWindowObject, + blockHash: BLOCK_HASH, + hydratedTx: boolean = false +): Promise { + return ewo.request({ method: 'eth_getBlockByHash', params: [blockHash, hydratedTx] }); +} + +export function getBlockByNumber( + ewo: EthereumWindowObject, + blockNumber: BLOCK_NUMBER | BLOCK_TAG, + hydratedTx: boolean = false +): Promise { + return ewo.request({ method: 'eth_getBlockByNumber', params: [blockNumber, hydratedTx] }); +} + +export function getBlockTransactionCountByHash( + ewo: EthereumWindowObject, + blockHash: BLOCK_HASH +): Promise { + return ewo.request({ method: 'eth_getBlockTransactionCountByHash', params: [blockHash] }); +} + +export function getBlockTransactionCountByNumber( + ewo: EthereumWindowObject, + blockNumber: BLOCK_NUMBER | BLOCK_TAG +): Promise { + return ewo.request({ method: 'eth_getBlockTransactionCountByNumber', params: [blockNumber] }); +} + +export function getCode( + ewo: EthereumWindowObject, + address: string, + block: BLOCK_HASH | BLOCK_NUMBER | BLOCK_TAG = 'latest' +): Promise { + return ewo.request({ method: 'eth_getCode', params: [address, block] }); +} + +export function getTransactionHashByBlockHashAndIndex( + ewo: EthereumWindowObject, + blockHash: BLOCK_HASH, + index: string +): Promise { + return ewo.request({ + method: 'eth_getTransactionByBlockHashAndIndex', + params: [blockHash, index], + }); +} + +export function getTransactionHashByBlockNumberAndIndex( + ewo: EthereumWindowObject, + blockNumber: BLOCK_NUMBER | BLOCK_TAG, + index: string +): Promise { + return ewo.request({ + method: 'eth_getTransactionByBlockNumberAndIndex', + params: [blockNumber, index], + }); +} + +export function getTransactionByHash(ewo: EthereumWindowObject, txHash: TXN_HASH): Promise { + return ewo.request({ method: 'eth_getTransactionByHash', params: [txHash] }); +} + +export function getTransactionCount( + ewo: EthereumWindowObject, + address: string, + block: BLOCK_HASH | BLOCK_NUMBER | BLOCK_TAG = 'latest' +): Promise { + return ewo.request({ method: 'eth_getTransactionCount', params: [address, block] }); +} + +export function getTransactionReceipt( + ewo: EthereumWindowObject, + txHash: TXN_HASH +): Promise { + return ewo.request({ method: 'eth_getTransactionReceipt', params: [txHash] }); +} + +export function syncing(ewo: EthereumWindowObject): Promise { + return ewo.request({ method: 'eth_syncing' }); +} + +/** + * Sign typed data using the wallet. + * @param ewo wallet window object to request the signature. + * @param message The typed data to sign. + * @param address The wallet address to sign. + * @returns Signatures as strings. + */ +export function signMessage( + ewo: EthereumWindowObject, + message: TypedData, + address: string +): Promise { + return ewo.request({ method: 'eth_signTypedData_v4', params: [address, message] }); +}