diff --git a/src/provider/abstractProvider.ts b/src/provider/abstractProvider.ts index 795f55ea4..ca23e931c 100644 --- a/src/provider/abstractProvider.ts +++ b/src/provider/abstractProvider.ts @@ -1,3 +1,4 @@ +import { CompiledContract } from '../types'; import { BigNumberish } from '../utils/number'; import { BlockIdentifier } from './utils'; @@ -21,42 +22,16 @@ type Status = */ export interface GetBlockResponse { - acceptedTime: number; // "timestamp" + acceptedTime: number; blockHash: string; blockNumber: number; gasPrice: string; - newRoot: string; // "state_root" - oldRoot?: string; // missing - parentHash: string; // "parent_block_hash" - sequencer: string; // "sequencer_address" - status: Status; - transactions: Array; -} - -/** - * getStateUpdate response object - */ - -export interface GetStateUpdateResponse { - blockHash: string; newRoot: string; - oldRoot: string; - acceptedTime?: number; // missing on the default provider - stateDiff: { - storageDiffs: Array<{ - address: string; - key: string; - value: string; - }>; - deployedContracts: Array<{ - address: string; - contractHash: string; - }>; - nonces?: Array<{ - contractAddress: string; - nonce: BigNumberish; - }>; // missing on the default provider - }; + oldRoot?: string; + parentHash: string; + sequencer: string; + status: Status; + transactions: Array; } /** @@ -67,10 +42,10 @@ export interface GetStateUpdateResponse { export type GetTransactionResponse = InvokeTransactionResponse & DeclareTransactionResponse; export interface CommonTransactionResponse { - transactionHash: string; - maxFee: string; - version: string; - signature: Array; + transactionHash?: string; + version?: string; + signature?: Array; + maxFee?: string; nonce?: string; } @@ -99,7 +74,7 @@ export interface ContractClass { } export interface DeclareTransactionResponse extends CommonTransactionResponse { - contractClass?: ContractClass; + contractClass?: any; senderAddress?: string; } @@ -109,9 +84,9 @@ export type GetTransactionReceiptResponse = export interface CommonTransactionReceiptResponse { transactionHash: string; - actualFee: string; status: Status; - statusData: string; + actualFee?: string; + statusData?: string; } export interface MessageToL1 { @@ -178,9 +153,6 @@ export abstract class Provider { // Get block information given the block hash or number abstract getBlock(blockIdentifier: BlockIdentifier): Promise; - // Get the information about the result of executing the requested block - abstract getStateUpdate(blockHash: BigNumberish): Promise; - // Get the value of the storage at the given address and key abstract getStorageAt( contractAddress: string, @@ -194,15 +166,6 @@ export abstract class Provider { // Get the transaction receipt by the transaction hash abstract getTransactionReceipt(txHash: BigNumberish): Promise; - // Get the contract class deployed under the given class hash. - abstract getClass(classHash: BigNumberish): Promise; - - // Get the contract class deployed under the given address. - abstract getClassAt(contractAddress: BigNumberish): Promise; - - // Get the class hash deployed under the given address. - abstract getClassHash(contractAddress: BigNumberish): Promise; - // Estimates the resources required by a transaction relative to a given state abstract estimateFee( request: FunctionCall, @@ -222,13 +185,13 @@ export abstract class Provider { ): Promise; abstract deployContract( - contractClass: ContractClass, + compiledContract: CompiledContract | string, constructorCalldata: Array, salt?: BigNumberish ): Promise; abstract declareContract( - contractClass: ContractClass, + compiledContract: CompiledContract | string, version?: BigNumberish ): Promise; diff --git a/src/provider/gateway.ts b/src/provider/gateway.ts new file mode 100644 index 000000000..6ee64bd85 --- /dev/null +++ b/src/provider/gateway.ts @@ -0,0 +1,366 @@ +import urljoin from 'url-join'; + +import { ONE, StarknetChainId, ZERO } from '../constants'; +import { CompiledContract, GetTransactionStatusResponse } from '../types'; +import { Gateway } from '../types/api/gateway'; +import { getSelectorFromName } from '../utils/hash'; +import { parse, parseAlwaysAsBig, stringify } from '../utils/json'; +import { BigNumberish, bigNumberishArrayToDecimalStringArray, toBN, toHex } from '../utils/number'; +import { compressProgram, randomAddress } from '../utils/stark'; +import { + CallContractResponse, + DeclareContractResponse, + DeployContractResponse, + FeeEstimateResponse, + FunctionCall, + GetBlockResponse, + GetTransactionReceiptResponse, + GetTransactionResponse, + InvokeContractResponse, + Provider, +} from './abstractProvider'; +import { ProviderOptions } from './default'; +import { GatewayError, HttpError } from './errors'; +import { GatewayAPIResponseParser } from './gatewayParser'; +import { BlockIdentifier, getFormattedBlockIdentifier } from './utils'; + +type NetworkName = 'mainnet-alpha' | 'goerli-alpha'; + +function wait(delay: number) { + return new Promise((res) => { + setTimeout(res, delay); + }); +} + +function isEmptyQueryObject(obj?: Record): obj is undefined { + return ( + obj === undefined || + Object.keys(obj).length === 0 || + (Object.keys(obj).length === 1 && + Object.entries(obj).every(([k, v]) => k === 'blockIdentifier' && v === null)) + ); +} + +export class GatewayProvider implements Provider { + public baseUrl: string; + + public feederGatewayUrl: string; + + public gatewayUrl: string; + + public chainId: StarknetChainId; + + private responseParser = new GatewayAPIResponseParser(); + + constructor(optionsOrProvider: ProviderOptions = { network: 'goerli-alpha' }) { + if ('network' in optionsOrProvider) { + this.baseUrl = GatewayProvider.getNetworkFromName(optionsOrProvider.network); + this.chainId = GatewayProvider.getChainIdFromBaseUrl(this.baseUrl); + this.feederGatewayUrl = urljoin(this.baseUrl, 'feeder_gateway'); + this.gatewayUrl = urljoin(this.baseUrl, 'gateway'); + } else { + this.baseUrl = optionsOrProvider.baseUrl; + this.feederGatewayUrl = + optionsOrProvider.feederGatewayUrl ?? urljoin(this.baseUrl, 'feeder_gateway'); + this.gatewayUrl = optionsOrProvider.gatewayUrl ?? urljoin(this.baseUrl, 'gateway'); + this.chainId = + optionsOrProvider.chainId ?? + GatewayProvider.getChainIdFromBaseUrl(optionsOrProvider.baseUrl); + } + } + + protected static getNetworkFromName(name: NetworkName) { + switch (name) { + case 'mainnet-alpha': + return 'https://alpha-mainnet.starknet.io'; + case 'goerli-alpha': + default: + return 'https://alpha4.starknet.io'; + } + } + + protected static getChainIdFromBaseUrl(baseUrl: string): StarknetChainId { + try { + const url = new URL(baseUrl); + if (url.host.includes('mainnet.starknet.io')) { + return StarknetChainId.MAINNET; + } + } catch { + // eslint-disable-next-line no-console + console.error(`Could not parse baseUrl: ${baseUrl}`); + } + return StarknetChainId.TESTNET; + } + + private getFetchUrl(endpoint: keyof Gateway.Endpoints) { + const gatewayUrlEndpoints = ['add_transaction']; + + return gatewayUrlEndpoints.includes(endpoint) ? this.gatewayUrl : this.feederGatewayUrl; + } + + private getFetchMethod(endpoint: keyof Gateway.Endpoints) { + const postMethodEndpoints = ['add_transaction', 'call_contract', 'estimate_fee']; + + return postMethodEndpoints.includes(endpoint) ? 'POST' : 'GET'; + } + + private getQueryString(query?: Record): string { + if (isEmptyQueryObject(query)) { + return ''; + } + const queryString = Object.entries(query) + .map(([key, value]) => { + if (key === 'blockIdentifier') { + return `${getFormattedBlockIdentifier(value)}`; + } + return `${key}=${value}`; + }) + .join('&'); + + return `?${queryString}`; + } + + private getHeaders(method: 'POST' | 'GET'): Record | undefined { + if (method === 'POST') { + return { + 'Content-Type': 'application/json', + }; + } + return undefined; + } + + // typesafe fetch + protected async fetchEndpoint( + endpoint: T, + // typescript type magiuc to create a nice fitting function interface + ...[query, request]: Gateway.Endpoints[T]['QUERY'] extends never + ? Gateway.Endpoints[T]['REQUEST'] extends never + ? [] // when no query and no request is needed, we can omit the query and request parameters + : [undefined, Gateway.Endpoints[T]['REQUEST']] + : Gateway.Endpoints[T]['REQUEST'] extends never + ? [Gateway.Endpoints[T]['QUERY']] // when no request is needed, we can omit the request parameter + : [Gateway.Endpoints[T]['QUERY'], Gateway.Endpoints[T]['REQUEST']] // when both query and request are needed, we cant omit anything + ): Promise { + const baseUrl = this.getFetchUrl(endpoint); + const method = this.getFetchMethod(endpoint); + const queryString = this.getQueryString(query); + const headers = this.getHeaders(method); + const url = urljoin(baseUrl, endpoint, queryString); + + try { + const res = await fetch(url, { + method, + body: stringify(request), + headers, + }); + const textResponse = await res.text(); + if (!res.ok) { + // This will allow user to handle contract errors + let responseBody: any; + try { + responseBody = parse(textResponse); + } catch { + // if error parsing fails, return an http error + throw new HttpError(res.statusText, res.status); + } + + const errorCode = responseBody.code || ((responseBody as any)?.status_code as string); // starknet-devnet uses status_code instead of code; They need to fix that + throw new GatewayError(responseBody.message, errorCode); // Caught locally, and re-thrown for the user + } + + if (endpoint === 'estimate_fee') { + return parseAlwaysAsBig(textResponse, (_, v) => { + if (v && typeof v === 'bigint') { + return toBN(v.toString()); + } + return v; + }); + } + return parse(textResponse) as Gateway.Endpoints[T]['RESPONSE']; + } catch (err) { + // rethrow custom errors + if (err instanceof GatewayError || err instanceof HttpError) { + throw err; + } + if (err instanceof Error) { + throw Error(`Could not ${method} from endpoint \`${url}\`: ${err.message}`); + } + throw err; + } + } + + public async callContract( + { contractAddress, entryPointSelector, calldata = [] }: FunctionCall, + blockIdentifier: BlockIdentifier = 'pending' + ): Promise { + return this.fetchEndpoint( + 'call_contract', + { blockIdentifier }, + { + signature: [], + contract_address: contractAddress, + entry_point_selector: getSelectorFromName(entryPointSelector), + calldata, + } + ).then(this.responseParser.parseCallContractResponse); + } + + public async getBlock(blockIdentifier: BlockIdentifier = 'pending'): Promise { + return this.fetchEndpoint('get_block', { blockIdentifier }).then( + this.responseParser.parseGetBlockResponse + ); + } + + public async getStorageAt( + contractAddress: string, + key: BigNumberish, + blockIdentifier: BlockIdentifier = 'pending' + ): Promise { + return this.fetchEndpoint('get_storage_at', { blockIdentifier, contractAddress, key }) as any; + } + + public async getTransaction(txHash: BigNumberish): Promise { + const txHashHex = toHex(toBN(txHash)); + return this.fetchEndpoint('get_transaction', { transactionHash: txHashHex }).then((value) => + this.responseParser.parseGetTransactionResponse(value) + ); + } + + public async getTransactionReceipt(txHash: BigNumberish): Promise { + const txHashHex = toHex(toBN(txHash)); + return this.fetchEndpoint('get_transaction_receipt', { transactionHash: txHashHex }).then( + this.responseParser.parseGetTransactionReceiptResponse + ); + } + + public async getClassAt( + contractAddress: string, + blockIdentifier: BlockIdentifier = 'pending' + ): Promise { + return this.fetchEndpoint('get_full_contract', { blockIdentifier, contractAddress }).then( + (res) => { + const parsedContract = typeof res === 'string' ? (parse(res) as CompiledContract) : res; + return { + ...parsedContract, + program: compressProgram(parsedContract.program), + }; + } + ); + } + + public async invokeContract( + functionInvocation: FunctionCall, + signature?: BigNumberish[] | undefined, + maxFee?: BigNumberish | undefined, + version?: BigNumberish | undefined + ): Promise { + return this.fetchEndpoint('add_transaction', undefined, { + type: 'INVOKE_FUNCTION', + contract_address: functionInvocation.contractAddress, + entry_point_selector: getSelectorFromName(functionInvocation.entryPointSelector), + calldata: bigNumberishArrayToDecimalStringArray(functionInvocation.calldata ?? []), + signature: bigNumberishArrayToDecimalStringArray(signature ?? []), + max_fee: maxFee, + version, + }).then(this.responseParser.parseInvokeContractResponse); + } + + public async deployContract( + compiledContract: CompiledContract | string, + constructorCalldata?: BigNumberish[], + salt?: BigNumberish | undefined + ): Promise { + const parsedContract = + typeof compiledContract === 'string' + ? (parse(compiledContract) as CompiledContract) + : compiledContract; + const contractDefinition = { + ...parsedContract, + program: compressProgram(parsedContract.program), + }; + + return this.fetchEndpoint('add_transaction', undefined, { + type: 'DEPLOY', + contract_address_salt: salt ?? randomAddress(), + constructor_calldata: bigNumberishArrayToDecimalStringArray(constructorCalldata ?? []), + contract_definition: contractDefinition, + }).then(this.responseParser.parseDeployContractResponse); + } + + public async declareContract( + compiledContract: CompiledContract | string, + _version?: BigNumberish | undefined + ): Promise { + const parsedContract = + typeof compiledContract === 'string' + ? (parse(compiledContract) as CompiledContract) + : compiledContract; + const contractDefinition = { + ...parsedContract, + program: compressProgram(parsedContract.program), + }; + + return this.fetchEndpoint('add_transaction', undefined, { + type: 'DECLARE', + contract_class: contractDefinition, + nonce: toHex(ZERO), + signature: [], + sender_address: toHex(ONE), + }).then(this.responseParser.parseDeclareContractResponse); + } + + public async estimateFee( + request: FunctionCall, + blockIdentifier: BlockIdentifier = 'pending', + signature?: Array + ): Promise { + return this.fetchEndpoint( + 'estimate_fee', + { blockIdentifier }, + { + contract_address: request.contractAddress, + entry_point_selector: getSelectorFromName(request.entryPointSelector), + calldata: bigNumberishArrayToDecimalStringArray(request.calldata ?? []), + signature: bigNumberishArrayToDecimalStringArray(signature || []), + } + ).then(this.responseParser.parseFeeEstimateResponse); + } + + /** + * Gets the status of a transaction. + * + * [Reference](https://github.com/starkware-libs/cairo-lang/blob/f464ec4797361b6be8989e36e02ec690e74ef285/src/starkware/starknet/services/api/feeder_gateway/feeder_gateway_client.py#L48-L52) + * + * @param txHash + * @returns the transaction status object { block_number, tx_status: NOT_RECEIVED | RECEIVED | PENDING | REJECTED | ACCEPTED_ONCHAIN } + */ + public async getTransactionStatus(txHash: BigNumberish): Promise { + const txHashHex = toHex(toBN(txHash)); + return this.fetchEndpoint('get_transaction_status', { transactionHash: txHashHex }); + } + + public async waitForTransaction(txHash: BigNumberish, retryInterval: number = 8000) { + let onchain = false; + + while (!onchain) { + // eslint-disable-next-line no-await-in-loop + await wait(retryInterval); + // eslint-disable-next-line no-await-in-loop + const res = await this.getTransactionStatus(txHash); + + const successStates = ['ACCEPTED_ON_L1', 'ACCEPTED_ON_L2', 'PENDING']; + const errorStates = ['REJECTED', 'NOT_RECEIVED']; + + if (successStates.includes(res.tx_status)) { + onchain = true; + } else if (errorStates.includes(res.tx_status)) { + const message = res.tx_failure_reason + ? `${res.tx_status}: ${res.tx_failure_reason.code}\n${res.tx_failure_reason.error_message}` + : res.tx_status; + const error = new Error(message) as Error & { response: GetTransactionStatusResponse }; + error.response = res; + throw error; + } + } + } +} diff --git a/src/provider/gatewayParser.ts b/src/provider/gatewayParser.ts new file mode 100644 index 000000000..012fc4503 --- /dev/null +++ b/src/provider/gatewayParser.ts @@ -0,0 +1,105 @@ +import { Gateway } from '../types/api/gateway'; +import { toBN } from '../utils/number'; +import { + CallContractResponse, + DeclareContractResponse, + DeployContractResponse, + FeeEstimateResponse, + GetBlockResponse, + GetTransactionReceiptResponse, + GetTransactionResponse, + InvokeContractResponse, +} from './abstractProvider'; +import { ResponseParser } from './parser'; + +export class GatewayAPIResponseParser extends ResponseParser { + public parseGetBlockResponse(res: Gateway.GetBlockResponse): GetBlockResponse { + return { + acceptedTime: res.timestamp, + blockHash: res.block_hash, + blockNumber: res.block_number, + gasPrice: res.gas_price, + newRoot: res.state_root, + oldRoot: undefined, + parentHash: res.parent_block_hash, + sequencer: res.sequencer_address, + status: res.status, + transactions: Object.values(res.transactions) + .map((value) => 'transaction_hash' in value && value.transaction_hash) + .filter(Boolean) as Array, + }; + } + + public parseGetTransactionResponse(res: Gateway.GetTransactionResponse): GetTransactionResponse { + return { + calldata: + 'calldata' in res.transaction ? (res.transaction.calldata as Array) : undefined, + contractAddress: + 'contract_address' in res.transaction ? res.transaction.contract_address : undefined, + contractClass: + 'contract_class' in res.transaction ? (res.transaction.contract_class as any) : undefined, + entryPointSelector: + 'entry_point_selector' in res.transaction + ? res.transaction.entry_point_selector + : undefined, + maxFee: 'max_fee' in res.transaction ? (res.transaction.max_fee as string) : undefined, + nonce: res.transaction.nonce as string, + senderAddress: + 'sender_address' in res.transaction + ? (res.transaction.sender_address as string) + : undefined, + signature: 'signature' in res.transaction ? res.transaction.signature : undefined, + transactionHash: + 'transaction_hash' in res.transaction ? res.transaction.transaction_hash : undefined, + version: 'version' in res.transaction ? (res.transaction.version as string) : undefined, + }; + } + + public parseGetTransactionReceiptResponse( + res: Gateway.TransactionReceiptResponse + ): GetTransactionReceiptResponse { + return { + transactionHash: res.transaction_hash, + actualFee: 'actual_fee' in res ? res.actual_fee : undefined, + status: res.status, + statusData: undefined, + messagesSent: res.l2_to_l1_messages as any, // TODO: parse + events: res.events as any, + l1OriginMessage: undefined, + }; + } + + public parseFeeEstimateResponse(res: Gateway.EstimateFeeResponse): FeeEstimateResponse { + return { + overallFee: toBN(res.amount), + }; + } + + public parseCallContractResponse(res: Gateway.CallContractResponse): CallContractResponse { + return { + result: res.result, + }; + } + + public parseInvokeContractResponse(res: Gateway.AddTransactionResponse): InvokeContractResponse { + return { + transactionHash: res.transaction_hash, + }; + } + + public parseDeployContractResponse(res: Gateway.AddTransactionResponse): DeployContractResponse { + return { + transactionHash: res.transaction_hash, + contractAddress: res.address as string, + }; + } + + public parseDeclareContractResponse( + res: Gateway.AddTransactionResponse + ): DeclareContractResponse { + return { + transactionHash: res.transaction_hash, + classHash: res.class_hash as string, + }; + } +} diff --git a/src/provider/index.ts b/src/provider/index.ts index 94a29c09c..abd957f32 100644 --- a/src/provider/index.ts +++ b/src/provider/index.ts @@ -2,6 +2,7 @@ import { Provider } from './default'; export * from './default'; export * from './errors'; +export * from './gateway'; export * from './interface'; export * from './rpcProvider'; diff --git a/src/provider/parser.ts b/src/provider/parser.ts index 587827fb9..cb586bedb 100644 --- a/src/provider/parser.ts +++ b/src/provider/parser.ts @@ -1,11 +1,9 @@ import { CallContractResponse, - ContractClass, DeclareContractResponse, DeployContractResponse, FeeEstimateResponse, GetBlockResponse, - GetStateUpdateResponse, GetTransactionReceiptResponse, GetTransactionResponse, InvokeContractResponse, @@ -14,10 +12,6 @@ import { export abstract class ResponseParser { abstract parseGetBlockResponse(res: any): GetBlockResponse; - abstract parseGetClassResponse(res: any): ContractClass; - - abstract parseGetStateUpdateResponse(res: any): GetStateUpdateResponse; - abstract parseGetTransactionResponse(res: any): GetTransactionResponse; abstract parseGetTransactionReceiptResponse(res: any): GetTransactionReceiptResponse; diff --git a/src/provider/rpcParser.ts b/src/provider/rpcParser.ts index 7a3aa6bf1..08fffb8a4 100644 --- a/src/provider/rpcParser.ts +++ b/src/provider/rpcParser.ts @@ -1,12 +1,10 @@ import { RPC } from '../types/api/rpc'; import { CallContractResponse, - ContractClass, DeclareContractResponse, DeployContractResponse, FeeEstimateResponse, GetBlockResponse, - GetStateUpdateResponse, GetTransactionReceiptResponse, GetTransactionResponse, InvokeContractResponse, @@ -29,45 +27,18 @@ export class RPCResponseParser extends ResponseParser { }; } - public parseGetClassResponse(res: RPC.GetClassResponse): ContractClass { - return { - program: res.program, - entryPointByType: res.entry_point_by_type, - }; - } - - public parseGetStateUpdateResponse(res: any): GetStateUpdateResponse { - return { - blockHash: res.block_hash, - newRoot: res.new_root, - oldRoot: res.old_root, - acceptedTime: res.accepted_time, - stateDiff: { - storageDiffs: res.storage_diffs, - deployedContracts: res.deployed_contracts.map((deployedContract: any) => ({ - address: deployedContract.address, - contractHash: deployedContract.contract_hash, - })), - nonces: res.nonces.map(({ contract_address, nonce }: any) => ({ - nonce, - contractAddress: contract_address, - })), - }, - }; - } - public parseGetTransactionResponse(res: RPC.GetTransactionResponse): GetTransactionResponse { return { - transactionHash: res.txn_hash, + calldata: res.calldata, + contractAddress: res.contract_address, + contractClass: res.contract_class, + entryPointSelector: res.entry_point_selector, maxFee: res.max_fee, nonce: res.nonce, + senderAddress: res.sender_address, signature: res.signature, + transactionHash: res.txn_hash, version: res.version, - senderAddress: res.sender_address, - contractClass: res.contract_class && this.parseGetClassResponse(res.contract_class), - contractAddress: res.contract_address, - entryPointSelector: res.entry_point_selector, - calldata: res.calldata, }; } diff --git a/src/provider/rpcProvider.ts b/src/provider/rpcProvider.ts index dadac784e..b115b75be 100644 --- a/src/provider/rpcProvider.ts +++ b/src/provider/rpcProvider.ts @@ -1,8 +1,10 @@ import fetch from 'cross-fetch'; import { StarknetChainId } from '../constants'; +import { CompiledContract } from '../types'; import { RPC } from '../types/api/rpc'; import { getSelectorFromName } from '../utils/hash'; +import { parse, stringify } from '../utils/json'; import { BigNumberish, bigNumberishArrayToDecimalStringArray, @@ -10,16 +12,14 @@ import { toBN, toHex, } from '../utils/number'; -import { randomAddress } from '../utils/stark'; +import { compressProgram, randomAddress } from '../utils/stark'; import { CallContractResponse, - ContractClass, DeclareContractResponse, DeployContractResponse, FeeEstimateResponse, FunctionCall, GetBlockResponse, - GetStateUpdateResponse, GetTransactionReceiptResponse, GetTransactionResponse, InvokeContractResponse, @@ -28,6 +28,12 @@ import { import { RPCResponseParser } from './rpcParser'; import { BlockIdentifier } from './utils'; +function wait(delay: number) { + return new Promise((res) => { + setTimeout(res, delay); + }); +} + export type RpcProviderOptions = { nodeUrl: string }; export class RPCProvider implements Provider { @@ -60,7 +66,7 @@ export class RPCProvider implements Provider { try { const rawResult = await fetch(this.nodeUrl, { method: 'POST', - body: JSON.stringify(requestData), + body: stringify(requestData), headers: { 'Content-Type': 'application/json', }, @@ -96,37 +102,15 @@ export class RPCProvider implements Provider { ); } - public async getClass(classHash: BigNumberish): Promise { - return this.fetchEndpoint('starknet_getClass', [classHash]).then( - this.responseParser.parseGetClassResponse - ); - } - - public async getClassHash(contractAddress: BigNumberish): Promise { - return this.fetchEndpoint('starknet_getClassHashAt', [contractAddress]); - } - - public async getClassAt(contractAddress: BigNumberish): Promise { - return this.fetchEndpoint('starknet_getClassAt', [contractAddress]).then( - this.responseParser.parseGetClassResponse - ); - } - public async getStorageAt( contractAddress: string, key: BigNumberish, - blockHash: BlockIdentifier + blockHash: BlockIdentifier = 'pending' ): Promise { const parsedKey = toHex(toBN(key)); return this.fetchEndpoint('starknet_getStorageAt', [contractAddress, parsedKey, blockHash]); } - public async getStateUpdate(blockHash: BigNumberish): Promise { - return this.fetchEndpoint('starknet_getStateUpdateByHash', [blockHash]).then( - this.responseParser.parseGetStateUpdateResponse - ); - } - public async getTransaction(txHash: BigNumberish): Promise { return this.fetchEndpoint('starknet_getTransactionByHash', [txHash]).then( this.responseParser.parseGetTransactionResponse @@ -139,9 +123,16 @@ export class RPCProvider implements Provider { ); } + public async getClassAt( + contractAddress: string, + _blockIdentifier: BlockIdentifier = 'pending' + ): Promise { + return this.fetchEndpoint('starknet_getClassAt', [contractAddress]); + } + public async estimateFee( request: FunctionCall, - blockIdentifier: BlockIdentifier + blockIdentifier: BlockIdentifier = 'pending' ): Promise { const parsedCalldata = request.calldata.map((data) => { if (typeof data === 'string' && isHex(data as string)) { @@ -161,29 +152,47 @@ export class RPCProvider implements Provider { } public async declareContract( - contractClass: ContractClass, - version?: BigNumberish | undefined + compiledContract: CompiledContract | string, + version: BigNumberish | undefined = 0 ): Promise { + const parsedContract = + typeof compiledContract === 'string' + ? (parse(compiledContract) as CompiledContract) + : compiledContract; + const contractDefinition = { + ...parsedContract, + program: compressProgram(parsedContract.program), + }; + return this.fetchEndpoint('starknet_addDeclareTransaction', [ { - program: contractClass.program, - entry_point_by_type: contractClass.entryPointByType, + program: contractDefinition.program, + entry_points_by_type: contractDefinition.entry_points_by_type, }, - version, + toHex(toBN(version)), ]).then(this.responseParser.parseDeclareContractResponse); } public async deployContract( - contractDefinition: ContractClass, - constructorCalldata: BigNumberish[], + compiledContract: CompiledContract | string, + constructorCalldata?: BigNumberish[], salt?: BigNumberish | undefined ): Promise { + const parsedContract = + typeof compiledContract === 'string' + ? (parse(compiledContract) as CompiledContract) + : compiledContract; + const contractDefinition = { + ...parsedContract, + program: compressProgram(parsedContract.program), + }; + return this.fetchEndpoint('starknet_addDeployTransaction', [ salt ?? randomAddress(), bigNumberishArrayToDecimalStringArray(constructorCalldata ?? []), { program: contractDefinition.program, - entry_point_by_type: contractDefinition.entryPointByType, + entry_points_by_type: contractDefinition.entry_points_by_type, }, ]).then(this.responseParser.parseDeployContractResponse); } @@ -236,8 +245,41 @@ export class RPCProvider implements Provider { return this.responseParser.parseCallContractResponse(result); } - public async waitForTransaction(txHash: BigNumberish, retryInterval: number): Promise { - throw new Error(`Not implemented ${txHash} ${retryInterval}`); + public async waitForTransaction(txHash: BigNumberish, retryInterval: number = 8000) { + let onchain = false; + // TODO: optimize this + let retries = 100; + + while (!onchain) { + const successStates = ['ACCEPTED_ON_L1', 'ACCEPTED_ON_L2', 'PENDING']; + const errorStates = ['REJECTED', 'NOT_RECEIVED']; + + // eslint-disable-next-line no-await-in-loop + await wait(retryInterval); + try { + // eslint-disable-next-line no-await-in-loop + const res = await this.getTransactionReceipt(txHash); + + if (successStates.includes(res.status)) { + onchain = true; + } else if (errorStates.includes(res.status)) { + const message = res.status; + const error = new Error(message) as Error & { response: any }; + error.response = res; + throw error; + } + } catch (error: unknown) { + if (error instanceof Error && errorStates.includes(error.message)) { + throw error; + } + + if (retries === 0) { + throw error; + } + } + + retries -= 1; + } } /** diff --git a/src/types/api/gateway.ts b/src/types/api/gateway.ts new file mode 100644 index 000000000..ff950bb31 --- /dev/null +++ b/src/types/api/gateway.ts @@ -0,0 +1,311 @@ +import BN from 'bn.js'; + +import { BlockIdentifier } from '../../provider/utils'; +import { BigNumberish } from '../../utils/number'; +import { + Abi, + BlockNumber, + CompressedCompiledContract, + EntryPointType, + RawCalldata, + Signature, + Status, + TransactionStatus, +} from '../lib'; + +export namespace Gateway { + export type GetContractAddressesResponse = { + Starknet: string; + GpsStatementVerifier: string; + }; + + export type DeclareTransaction = { + type: 'DECLARE'; + contract_class: CompressedCompiledContract; + nonce: BigNumberish; + sender_address: BigNumberish; + signature: Signature; + }; + + export type DeployTransaction = { + type: 'DEPLOY'; + contract_definition: CompressedCompiledContract; + contract_address_salt: BigNumberish; + constructor_calldata: string[]; + nonce?: BigNumberish; + }; + + export type InvokeFunctionTransaction = { + type: 'INVOKE_FUNCTION'; + contract_address: string; + signature?: Signature; + entry_point_type?: EntryPointType; + entry_point_selector: string; + calldata?: RawCalldata; + nonce?: BigNumberish; + max_fee?: BigNumberish; + version?: BigNumberish; + }; + + export type Transaction = DeclareTransaction | DeployTransaction | InvokeFunctionTransaction; + + export type AddTransactionResponse = { + transaction_hash: string; + code?: TransactionStatus; + address?: string; + class_hash?: string; + }; + + export interface InvokeFunctionTransactionResponse extends InvokeFunctionTransaction { + transaction_hash: string; + } + + export type TransactionResponse = + | DeclareTransaction + | DeployTransaction + | InvokeFunctionTransactionResponse; + + export type SuccessfulTransactionResponse = { + status: Status; + transaction: TransactionResponse; + block_hash: string; + block_number: BlockNumber; + transaction_index: number; + }; + + export type FailedTransactionResponse = { + status: 'REJECTED'; + transaction_failure_reason: { + code: string; + error_message: string; + }; + transaction: TransactionResponse; + }; + + export type GetTransactionResponse = SuccessfulTransactionResponse | FailedTransactionResponse; + + export type GetTransactionStatusResponse = { + tx_status: Status; + block_hash?: string; + tx_failure_reason?: { + code: string; + error_message: string; + }; + }; + + export type RawArgs = { + [inputName: string]: string | string[] | { type: 'struct'; [k: string]: BigNumberish }; + }; + + export type ExecutionResources = { + n_steps: number; + builtin_instance_counter: { + pedersen_builtin: number; + range_check_builtin: number; + bitwise_builtin: number; + output_builtin: number; + ecdsa_builtin: number; + ec_op_builtin?: number; + }; + n_memory_holes: number; + }; + + export type GetTransactionTraceResponse = { + function_invocation: { + caller_address: string; + contract_address: string; + code_address: string; + selector: string; + calldata: RawArgs; + result: Array; + execution_resources: ExecutionResources; + internal_call: Array; + events: Array; + messages: Array; + }; + signature: Signature; + }; + + export type TransactionReceiptResponse = + | SuccessfulTransactionReceiptResponse + | FailedTransactionReceiptResponse; + + export type SuccessfulTransactionReceiptResponse = { + status: Status; + transaction_hash: string; + transaction_index: number; + block_hash: string; + block_number: BlockNumber; + l2_to_l1_messages: string[]; + events: string[]; + actual_fee: string; + execution_resources: ExecutionResources; + }; + + export type FailedTransactionReceiptResponse = { + status: 'REJECTED'; + transaction_failure_reason: { + code: string; + error_message: string; + }; + transaction_hash: string; + l2_to_l1_messages: string[]; + events: string[]; + }; + + export type GetCodeResponse = { + bytecode: string[]; + abi: Abi; + }; + + export type GetBlockResponse = { + block_number: number; + state_root: string; + block_hash: string; + transactions: { + [txHash: string]: TransactionResponse; + }; + timestamp: number; + transaction_receipts: { + [txHash: string]: { + block_hash: string; + transaction_hash: string; + l2_to_l1_messages: { + to_address: string; + payload: string[]; + from_address: string; + }[]; + block_number: BlockNumber; + status: Status; + transaction_index: number; + }; + }; + parent_block_hash: string; + status: Status; + gas_price: string; + sequencer_address: string; + }; + + export type CallContractTransaction = Omit< + InvokeFunctionTransaction, + 'type' | 'entry_point_type' | 'nonce' + >; + + export type CallContractResponse = { + result: string[]; + }; + + export type EstimateFeeResponse = { + amount: BN; + unit: string; + }; + + export type Endpoints = { + get_contract_addresses: { + QUERY: never; + REQUEST: never; + RESPONSE: GetContractAddressesResponse; + }; + add_transaction: { + QUERY: never; + REQUEST: Transaction; + RESPONSE: AddTransactionResponse; + }; + get_transaction: { + QUERY: { + transactionHash: string; + }; + REQUEST: never; + RESPONSE: GetTransactionResponse; + }; + get_transaction_status: { + QUERY: { + transactionHash: string; + }; + REQUEST: never; + RESPONSE: GetTransactionStatusResponse; + }; + get_transaction_trace: { + QUERY: { + transactionHash: string; + }; + REQUEST: never; + RESPONSE: GetTransactionTraceResponse; + }; + get_transaction_receipt: { + QUERY: { + transactionHash: string; + }; + REQUEST: never; + RESPONSE: TransactionReceiptResponse; + }; + get_storage_at: { + QUERY: { + contractAddress: string; + key: BigNumberish; + blockIdentifier: BlockIdentifier; + }; + REQUEST: never; + RESPONSE: object; + }; + get_code: { + QUERY: { + contractAddress: string; + blockIdentifier: BlockIdentifier; + }; + REQUEST: never; + RESPONSE: GetCodeResponse; + }; + get_block: { + QUERY: { + blockIdentifier: BlockIdentifier; + }; + REQUEST: never; + RESPONSE: GetBlockResponse; + }; + call_contract: { + QUERY: { + blockIdentifier: BlockIdentifier; + }; + REQUEST: CallContractTransaction; + RESPONSE: CallContractResponse; + }; + estimate_fee: { + QUERY: { + blockIdentifier: BlockIdentifier; + }; + REQUEST: CallContractTransaction; + RESPONSE: EstimateFeeResponse; + }; + get_class_by_hash: { + QUERY: { + classHash: string; + }; + REQUEST: never; + RESPONSE: any; + }; + get_class_hash_at: { + QUERY: { + contractAddress: string; + blockIdentifier?: BlockIdentifier; + }; + REQUEST: never; + RESPONSE: string; + }; + get_state_update: { + QUERY: { + blockHash: string; + }; + REQUEST: never; + RESPONSE: any; + }; + get_full_contract: { + QUERY: { + contractAddress: string; + blockIdentifier?: BlockIdentifier; + }; + REQUEST: never; + RESPONSE: any; + }; + }; +} diff --git a/src/types/api/rpc.ts b/src/types/api/rpc.ts index 29cce0b21..2264f0a24 100644 --- a/src/types/api/rpc.ts +++ b/src/types/api/rpc.ts @@ -238,25 +238,10 @@ export namespace RPC { REQUEST: any[]; RESPONSE: DeclareResponse; }; - starknet_getClass: { - QUERY: never; - REQUEST: any[]; - RESPONSE: any; - }; starknet_getClassAt: { QUERY: never; REQUEST: any[]; RESPONSE: any; }; - starknet_getStateUpdateByHash: { - QUERY: never; - REQUEST: any[]; - RESPONSE: any; - }; - starknet_getClassHashAt: { - QUERY: never; - REQUEST: any[]; - RESPONSE: string; - }; }; }