diff --git a/__tests__/cairo1.test.ts b/__tests__/cairo1.test.ts index 24c3645d5..5dc320e02 100644 --- a/__tests__/cairo1.test.ts +++ b/__tests__/cairo1.test.ts @@ -60,6 +60,11 @@ describeIfDevnet('Cairo 1 Devnet', () => { expect(cairo1Contract).toBeInstanceOf(Contract); }); + test('getCairoVersion', async () => { + const version1 = await cairo1Contract.getVersion(); + expect(version1).toEqual({ cairo: '1', compiler: '1' }); + }); + test('ContractFactory on Cairo1', async () => { const c1CFactory = new ContractFactory({ compiledContract: compiledHelloSierra, diff --git a/__tests__/cairo1v2.test.ts b/__tests__/cairo1v2.test.ts index 54db9d0dc..06e92cde5 100644 --- a/__tests__/cairo1v2.test.ts +++ b/__tests__/cairo1v2.test.ts @@ -41,7 +41,7 @@ const { uint256, tuple, isCairo1Abi } = cairo; const { toHex } = num; const { starknetKeccak } = selector; -describe('Cairo 1 Devnet', () => { +describe('Cairo 1', () => { const provider = getTestProvider(); const account = getTestAccount(provider); describe('API & Contract interactions', () => { @@ -72,6 +72,14 @@ describe('Cairo 1 Devnet', () => { expect(cairo210Contract).toBeInstanceOf(Contract); }); + test('getCairoVersion', async () => { + const version1 = await cairo1Contract.getVersion(); + expect(version1).toEqual({ cairo: '1', compiler: '2' }); + + const version210 = await cairo210Contract.getVersion(); + expect(version210).toEqual({ cairo: '1', compiler: '2' }); + }); + xtest('validate TS for redeclare - skip testing', async () => { const cc0 = await account.getClassAt(dd.deploy.address); const cc0_1 = await account.getClassByHash(toHex(dd.declare.class_hash)); diff --git a/__tests__/contract.test.ts b/__tests__/contract.test.ts index 943399166..3fe078420 100644 --- a/__tests__/contract.test.ts +++ b/__tests__/contract.test.ts @@ -59,6 +59,11 @@ describe('contract module', () => { ); }); + test('getCairoVersion', async () => { + const version = await erc20Contract.getVersion(); + expect(version).toEqual({ cairo: '0', compiler: '0' }); + }); + test('isCairo1', async () => { const isContractCairo1: boolean = erc20Contract.isCairo1(); expect(isContractCairo1).toBe(false); diff --git a/__tests__/defaultProvider.test.ts b/__tests__/defaultProvider.test.ts index faa70524c..63f09780c 100644 --- a/__tests__/defaultProvider.test.ts +++ b/__tests__/defaultProvider.test.ts @@ -44,6 +44,11 @@ describe('defaultProvider', () => { expect(exampleTransactionHash).toBeTruthy(); }); + test('getContractVersion', async () => { + const version = await testProvider.getContractVersion(erc20ContractAddress); + expect(version).toEqual({ cairo: '0', compiler: '0' }); + }); + describe('getBlock', () => { test('getBlock(blockIdentifier=latest)', async () => { expect(exampleBlock).not.toBeNull(); diff --git a/src/contract/default.ts b/src/contract/default.ts index 9f2facf28..af6a0b832 100644 --- a/src/contract/default.ts +++ b/src/contract/default.ts @@ -342,6 +342,10 @@ export class Contract implements ContractInterface { return cairo.isCairo1Abi(this.abi); } + public async getVersion() { + return this.providerOrAccount.getContractVersion(this.address); + } + public typed(tAbi: TAbi): TypedContract { return this as TypedContract; } diff --git a/src/contract/interface.ts b/src/contract/interface.ts index d916026f2..94d14dcfe 100644 --- a/src/contract/interface.ts +++ b/src/contract/interface.ts @@ -9,6 +9,7 @@ import { BlockIdentifier, CallOptions, ContractFunction, + ContractVersion, EstimateFeeResponse, GetTransactionReceiptResponse, Invocation, @@ -132,5 +133,11 @@ export abstract class ContractInterface { */ public abstract isCairo1(): boolean; + /** + * Gets contract's version (cairo version & compiler version) + * @returns ContractVersion + */ + public abstract getVersion(): Promise; + public abstract typed(tAbi: TAbi): TypedContract; } diff --git a/src/provider/default.ts b/src/provider/default.ts index 445f0078c..567fe82b5 100644 --- a/src/provider/default.ts +++ b/src/provider/default.ts @@ -26,6 +26,7 @@ import { SimulateTransactionResponse, StateUpdateResponse, Storage, + getContractVersionOptions, getEstimateFeeBulkOptions, getSimulateTransactionOptions, waitForTransactionOptions, @@ -221,4 +222,8 @@ export class Provider implements ProviderInterface { public async getAddressFromStarkName(name: string, StarknetIdContract?: string): Promise { return getAddressFromStarkName(this, name, StarknetIdContract); } + + public async getContractVersion(contractAddress: string, options?: getContractVersionOptions) { + return this.provider.getContractVersion(contractAddress, options); + } } diff --git a/src/provider/interface.ts b/src/provider/interface.ts index 5e693384b..e963f9a4d 100644 --- a/src/provider/interface.ts +++ b/src/provider/interface.ts @@ -6,6 +6,7 @@ import type { Call, CallContractResponse, ContractClassResponse, + ContractVersion, DeclareContractResponse, DeclareContractTransaction, DeployAccountContractPayload, @@ -24,6 +25,7 @@ import type { SimulateTransactionResponse, StateUpdateResponse, Storage, + getContractVersionOptions, getEstimateFeeBulkOptions, getSimulateTransactionOptions, waitForTransactionOptions, @@ -330,4 +332,16 @@ export abstract class ProviderInterface { * @returns StateUpdateResponse */ public abstract getStateUpdate(blockIdentifier?: BlockIdentifier): Promise; + + /** + * Gets the contract version from the provided address + * @param contractAddress string + * @param options - getContractVersionOptions + * - (optional) compiler - (default true) extract compiler version using type tactic from abi + * - (optional) blockIdentifier - block identifier + */ + public abstract getContractVersion( + contractAddress: string, + options?: getContractVersionOptions + ): Promise; } diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index 2fe3500a1..24cf0be20 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -28,6 +28,7 @@ import { RpcProviderOptions, SimulateTransactionResponse, TransactionType, + getContractVersionOptions, getEstimateFeeBulkOptions, getSimulateTransactionOptions, waitForTransactionOptions, @@ -38,6 +39,7 @@ import { TransactionFinalityStatus, } from '../types/api/rpc'; import { CallData } from '../utils/calldata'; +import { getAbiContractVersion } from '../utils/calldata/cairo'; import { isSierra } from '../utils/contract'; import { pascalToSnake } from '../utils/encode'; import fetch from '../utils/fetchPonyfill'; @@ -251,6 +253,21 @@ export class RpcProvider implements ProviderInterface { throw new Error('RPC does not implement getCode function'); } + public async getContractVersion( + contractAddress: string, + { blockIdentifier = this.blockIdentifier, compiler = true }: getContractVersionOptions + ) { + const contractClass = await this.getClassAt(contractAddress, blockIdentifier); + if (isSierra(contractClass)) { + if (compiler) { + const abiTest = getAbiContractVersion(contractClass.abi); + return { cairo: '1', compiler: abiTest.compiler }; + } + return { cairo: '1', compiler: 'unknown' }; + } + return { cairo: '0', compiler: '0' }; + } + public async getEstimateFee( invocation: Invocation, invocationDetails: InvocationsDetailsWithNonce, diff --git a/src/provider/sequencer.ts b/src/provider/sequencer.ts index e95f60f17..6cefbc180 100644 --- a/src/provider/sequencer.ts +++ b/src/provider/sequencer.ts @@ -34,11 +34,13 @@ import { TransactionExecutionStatus, TransactionFinalityStatus, TransactionType, + getContractVersionOptions, getEstimateFeeBulkOptions, getSimulateTransactionOptions, waitForTransactionOptions, } from '../types'; import { CallData } from '../utils/calldata'; +import { getAbiContractVersion } from '../utils/calldata/cairo'; import { isSierra } from '../utils/contract'; import fetch from '../utils/fetchPonyfill'; import { @@ -342,6 +344,24 @@ export class SequencerProvider implements ProviderInterface { return this.fetchEndpoint('get_compiled_class_by_class_hash', { classHash, blockIdentifier }); } + public async getContractVersion( + contractAddress: string, + { blockIdentifier, compiler }: getContractVersionOptions = { + blockIdentifier: this.blockIdentifier, + compiler: true, + } + ) { + const contractClass = await this.getClassAt(contractAddress, blockIdentifier); + if (isSierra(contractClass)) { + if (compiler) { + const abiTest = getAbiContractVersion(contractClass.abi); + return { cairo: '1', compiler: abiTest.compiler }; + } + return { cairo: '1', compiler: 'unknown' }; + } + return { cairo: '0', compiler: '0' }; + } + public async invokeFunction( functionInvocation: Invocation, details: InvocationsDetailsWithNonce diff --git a/src/types/lib/index.ts b/src/types/lib/index.ts index 17f9aec3c..0091580f5 100644 --- a/src/types/lib/index.ts +++ b/src/types/lib/index.ts @@ -230,6 +230,11 @@ export type getSimulateTransactionOptions = { skipFeeCharge?: boolean; }; +export type getContractVersionOptions = { + blockIdentifier?: BlockIdentifier; + compiler?: boolean; +}; + export type getEstimateFeeBulkOptions = { blockIdentifier?: BlockIdentifier; skipValidate?: boolean; @@ -241,4 +246,14 @@ export interface CallStruct { calldata: string[]; } +/** + * Represent Contract version + * cairo: version of the cairo language + * compiler: version of the cairo compiler used to compile the contract + */ +export type ContractVersion = { + cairo: string | 'unknown'; + compiler: string | 'unknown'; +}; + export * from './contract'; diff --git a/src/utils/calldata/cairo.ts b/src/utils/calldata/cairo.ts index d5e2d439f..a9ac3bab1 100644 --- a/src/utils/calldata/cairo.ts +++ b/src/utils/calldata/cairo.ts @@ -1,4 +1,13 @@ -import { Abi, AbiEnums, AbiStructs, BigNumberish, Litteral, Uint, Uint256 } from '../../types'; +import { + Abi, + AbiEnums, + AbiStructs, + BigNumberish, + ContractVersion, + Litteral, + Uint, + Uint256, +} from '../../types'; import { isBigInt, isHex, isStringWholeNumber } from '../num'; import { encodeShortString, isShortString, isText } from '../shortString'; import { UINT_128_MAX, isUint256 } from '../uint256'; @@ -59,6 +68,33 @@ export function isCairo1Abi(abi: Abi): boolean { throw new Error(`Error in ABI. No input/output in function ${firstFunction.name}`); } +/** + * Return ContractVersion (Abi version) based on Abi + * or undefined for unknown version + * @param abi + * @returns string + */ +export function getAbiContractVersion(abi: Abi): ContractVersion { + // determine by interface for "Cairo 1.2" + if (abi.find((it) => it.type === 'interface')) { + return { cairo: '1', compiler: '2' }; + } + + // determine by function io types "Cairo 1.1" or "Cairo 0.0" + // find first function with inputs or outputs + const testFunction = abi.find( + (it) => it.type === 'function' && (it.inputs.length || it.outputs.length) + ); + if (!testFunction) { + return { cairo: 'unknown', compiler: 'unknown' }; + } + const io = testFunction.inputs.length ? testFunction.inputs : testFunction.outputs; + if (isCairo1Type(io[0].type)) { + return { cairo: '1', compiler: '1' }; + } + return { cairo: '0', compiler: '0' }; +} + /** * named tuple cairo type is described as js object {} * struct cairo type are described as js object {}