diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 1a4acea69..2ee6eb867 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} env: - DEVNET_SHA: 64c425b832b96ba09b49646fe0fbf49862c0fb6d + DEVNET_SHA: 85495efb71a37ad3921c8986474b7e78a9a9f5fc steps: - uses: actions/checkout@v3 with: diff --git a/.gitignore b/.gitignore index 56cb7e6c9..7a4cebe30 100644 --- a/.gitignore +++ b/.gitignore @@ -46,9 +46,7 @@ venv !gradle/wrapper/gradle-wrapper.jar # Exclude test and demo resources temp files -lib/src/test/resources/accounts* lib/src/test/resources/contracts*/target -lib/src/test/resources/compilers javademo/src/main/resources/contracts*/target test_variables.env diff --git a/androiddemo/build.gradle.kts b/androiddemo/build.gradle.kts index 23d22337e..369a9fb52 100644 --- a/androiddemo/build.gradle.kts +++ b/androiddemo/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { implementation("com.google.android.material:material:1.9.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") // Make sure you are using the AAR and not a JAR and include transitive dependencies - implementation("com.swmansion.starknet:starknet:0.8.2@aar"){ + implementation("com.swmansion.starknet:starknet:0.9.0@aar"){ isTransitive = true } implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") diff --git a/javademo/build.gradle.kts b/javademo/build.gradle.kts index 36b9cef29..6af6acc87 100644 --- a/javademo/build.gradle.kts +++ b/javademo/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } dependencies { - implementation("com.swmansion.starknet:starknet:0.8.2") + implementation("com.swmansion.starknet:starknet:0.9.0") testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0") testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0") } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 908682671..394d9de0e 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -8,7 +8,7 @@ import org.jetbrains.dokka.gradle.DokkaTask -version = "0.8.2" +version = "0.9.0" group = "com.swmansion.starknet" plugins { diff --git a/lib/src/main/kotlin/com/swmansion/starknet/account/Account.kt b/lib/src/main/kotlin/com/swmansion/starknet/account/Account.kt index 2f1417fcf..7ed3969ad 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/account/Account.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/account/Account.kt @@ -14,59 +14,111 @@ interface Account { val address: Felt /** - * Sign a transaction. + * Sign a version 1 invoke transaction. * * Sign a transaction to be executed on Starknet. * * @param call a call to be signed * @param params additional execution parameters for the transaction * @param forFeeEstimate when set to `true`, it changes the version to `2^128+version` so the signed transaction can only be used for fee estimation - * @return signed invoke function payload + * @return signed invoke transaction version 1 payload */ - fun sign(call: Call, params: ExecutionParams, forFeeEstimate: Boolean): InvokeTransactionPayload { + fun sign(call: Call, params: ExecutionParams, forFeeEstimate: Boolean): InvokeTransactionV1Payload { return sign(listOf(call), params, forFeeEstimate) } /** - * Sign a transaction. + * Sign a version 3 invoke transaction. * * Sign a transaction to be executed on Starknet. * * @param call a call to be signed * @param params additional execution parameters for the transaction - * @return signed invoke function payload + * @param forFeeEstimate when set to `true`, it changes the version to `2^128+version` so the signed transaction can only be used for fee estimation + * @return signed invoke transaction version 3 payload + */ + fun sign(call: Call, params: ExecutionParamsV3, forFeeEstimate: Boolean): InvokeTransactionV3Payload { + return sign(listOf(call), params, forFeeEstimate) + } + + /** + * Sign a version 1 invoke transaction. + * + * Sign a transaction to be executed on Starknet. + * + * @param call a call to be signed + * @param params additional execution parameters for the transaction + * @return signed invoke transaction version 1 payload */ - fun sign(call: Call, params: ExecutionParams): InvokeTransactionPayload { + fun sign(call: Call, params: ExecutionParams): InvokeTransactionV1Payload { return sign(listOf(call), params, false) } /** - * Sign multiple calls as a single transaction. + * Sign a version 3 invoke transaction. + * + * Sign a transaction to be executed on Starknet. + * + * @param call a call to be signed + * @param params additional execution parameters for the transaction + * @return signed invoke transaction version 3 payload + */ + fun sign(call: Call, params: ExecutionParamsV3): InvokeTransactionV3Payload { + return sign(listOf(call), params, false) + } + + /** + * Sign multiple calls as a single version 1 invoke transaction. + * + * Sign a list of calls to be executed on Starknet. + * + * @param calls a list of calls to be signed + * @param params additional execution parameters for the transaction + * @param forFeeEstimate when set to `true`, it changes the version to `2^128+version` so the signed transaction can only be used for fee estimation + * @return signed invoke transaction version 1 payload + */ + fun sign(calls: List, params: ExecutionParams, forFeeEstimate: Boolean): InvokeTransactionV1Payload + + /** + * Sign multiple calls as a single version 3 invoke transaction. * * Sign a list of calls to be executed on Starknet. * * @param calls a list of calls to be signed * @param params additional execution parameters for the transaction * @param forFeeEstimate when set to `true`, it changes the version to `2^128+version` so the signed transaction can only be used for fee estimation - * @return signed invoke function payload + * @return signed invoke transaction version 3 payload */ - fun sign(calls: List, params: ExecutionParams, forFeeEstimate: Boolean): InvokeTransactionPayload + fun sign(calls: List, params: ExecutionParamsV3, forFeeEstimate: Boolean): InvokeTransactionV3Payload /** - * Sign multiple calls as a single transaction. + * Sign multiple calls as a single version 1 invoke transaction. * * Sign a list of calls to be executed on Starknet. * * @param calls a list of calls to be signed * @param params additional execution parameters for the transaction - * @return signed invoke function payload + * @return signed invoke transaction version 1 payload */ - fun sign(calls: List, params: ExecutionParams): InvokeTransactionPayload { + fun sign(calls: List, params: ExecutionParams): InvokeTransactionV1Payload { return sign(calls, params, false) } /** - * Sign deploy account transaction. + * Sign multiple calls as a single version 3 invoke transaction. + * + * Sign a list of calls to be executed on Starknet. + * + * @param calls a list of calls to be signed + * @param params additional execution parameters for the transaction + * @return signed invoke transaction version 3 payload + */ + fun sign(calls: List, params: ExecutionParamsV3): InvokeTransactionV3Payload { + return sign(calls, params, false) + } + + /** + * Sign version 1 deploy account transaction. * * Sign a deploy account transaction that requires prefunding deployed address. * @@ -85,7 +137,27 @@ interface Account { maxFee: Felt, nonce: Felt, forFeeEstimate: Boolean, - ): DeployAccountTransactionPayload + ): DeployAccountTransactionV1Payload + + /** + * Sign version 3 deploy account transaction. + * + * Sign a deploy account transaction that requires prefunding said account. + * + * @param classHash hash of the contract that will be deployed + * @param calldata constructor calldata for the contract deployment + * @param salt salt used to calculate address of the new contract + * @param params additional params for the transaction + * @param forFeeEstimate when set to `true`, it changes the version to `2^128+version` so the signed transaction can only be used for fee estimation + * @return signed deploy account payload + */ + fun signDeployAccount( + classHash: Felt, + calldata: Calldata, + salt: Felt, + params: DeployAccountParamsV3, + forFeeEstimate: Boolean = false, + ): DeployAccountTransactionV3Payload /** * Sign deploy account transaction. @@ -103,10 +175,35 @@ interface Account { calldata: Calldata, salt: Felt, maxFee: Felt, - ): DeployAccountTransactionPayload { + ): DeployAccountTransactionV1Payload { return signDeployAccount(classHash, calldata, salt, maxFee, Felt.ZERO, false) } + /** + * Sign version 3 deploy account transaction. + * + * Sign a deploy account transaction that requires prefunding deployed address. + * + * @param classHash hash of the contract that will be deployed. Has to be declared first! + * @param calldata constructor calldata for the contract deployment + * @param salt salt used to calculate address of the new contract + * @param forFeeEstimate when set to `true`, it changes the version to `2^128+version` so the signed transaction can only be used for fee estimation + * @return signed deploy account payload + */ + fun signDeployAccount( + classHash: Felt, + calldata: Calldata, + salt: Felt, + l1ResourceBounds: ResourceBounds, + forFeeEstimate: Boolean = false, + ): DeployAccountTransactionV3Payload { + val params = DeployAccountParamsV3( + nonce = Felt.ZERO, + l1ResourceBounds = l1ResourceBounds, + ) + return signDeployAccount(classHash, calldata, salt, params, forFeeEstimate) + } + /** * Sign a version 1 declare transaction. * @@ -143,6 +240,24 @@ interface Account { forFeeEstimate: Boolean = false, ): DeclareTransactionV2Payload + /** + * Sign a version 3 declare transaction. + * + * Prepare and sign a version 3 declare transaction to be executed on Starknet. + * + * @param sierraContractDefinition a cairo 1/2 sierra compiled definition of the contract to be declared + * @param casmContractDefinition a casm representation of cairo 1/2 compiled contract to be declared + * @param params additional parameters for the transaction + * @param forFeeEstimate when set to `true`, it changes the version to `2^128+version` so the signed transaction can only be used for fee estimation + * @return signed declare transaction payload + */ + fun signDeclare( + sierraContractDefinition: Cairo1ContractDefinition, + casmContractDefinition: CasmContractDefinition, + params: DeclareParamsV3, + forFeeEstimate: Boolean = false, + ): DeclareTransactionV3Payload + /** * Sign TypedData for off-chain usage with this account privateKey * @@ -161,9 +276,9 @@ interface Account { fun verifyTypedDataSignature(typedData: TypedData, signature: Signature): Request /** - * Execute a list of calls + * Execute a list of calls using version 1 invoke transaction. * - * Execute list of calls on starknet. + * Execute list of calls on Starknet. * * @param calls a list of calls to be executed. * @param maxFee a max fee to pay for the transaction. @@ -172,7 +287,18 @@ interface Account { fun execute(calls: List, maxFee: Felt): Request /** - * Execute single call. + * Execute a list of calls using version 3 invoke transaction. + * + * Execute list of calls on Starknet. + * + * @param calls a list of calls to be executed. + * @param l1ResourceBounds L1 resource bounds for the transaction. + * @return Invoke function response, containing transaction hash. + */ + fun executeV3(calls: List, l1ResourceBounds: ResourceBounds): Request + + /** + * Execute single call using version 1 invoke transaction. * * Execute single call on starknet. * @@ -180,12 +306,21 @@ interface Account { * @param maxFee a max fee to pay for the transaction. * @return Invoke function response, containing transaction hash. */ - fun execute(call: Call, maxFee: Felt): Request { - return execute(listOf(call), maxFee) - } + fun execute(call: Call, maxFee: Felt): Request /** - * Execute a list of calls with automatically estimated fee. + * Execute single call using version 3 invoke transaction. + * + * Execute single call on starknet. + * + * @param call a call to be executed. + * @param l1ResourceBounds L1 resource bounds for the transaction. + * @return Invoke function response, containing transaction hash. + */ + fun executeV3(call: Call, l1ResourceBounds: ResourceBounds): Request + + /** + * Execute a list of calls with automatically estimated fee using version 1 invoke transaction. * * @param calls a list of calls to be executed. * @return Invoke function response, containing transaction hash. @@ -193,26 +328,82 @@ interface Account { fun execute(calls: List): Request /** - * Execute single call with automatically estimated fee + * Execute a list of calls with automatically estimated fee using version 3 invoke transaction. + * + * @param calls a list of calls to be executed. + * @return Invoke function response, containing transaction hash. + */ + fun executeV3(calls: List): Request + + /** + * Execute single call with automatically estimated fee using version 1 invoke transaction. * * @param call a call to be executed. * @return Invoke function response, containing transaction hash. */ - fun execute(call: Call): Request { - return execute(listOf(call)) - } + fun execute(call: Call): Request /** - * Estimate fee for a call. + * Execute single call with automatically estimated fee using version 3 invoke transaction. + * + * @param call a call to be executed. + * @return Invoke function response, containing transaction hash. + */ + fun executeV3(call: Call): Request + + /** + * Estimate fee for a call as a version 1 invoke transaction. * * Estimate fee for a signed call on starknet. * * @param call a call used to estimate a fee. * @return Field value representing estimated fee. */ - fun estimateFee(call: Call): Request> { - return estimateFee(listOf(call)) - } + fun estimateFee(call: Call): Request> + + /** + * Estimate fee for a call as a version 1 invoke transaction. + * + * Estimate fee for a signed call on starknet. + * + * @param call a call used to estimate a fee. + * @param simulationFlags a set of simulation flags used to estimate a fee. + * @return Field value representing estimated fee. + */ + fun estimateFee(call: Call, simulationFlags: Set): Request> + + /** + * Estimate fee for a call as a version 3 invoke transaction. + * + * Estimate fee for a signed call on starknet. + * + * @param call a call used to estimate a fee. + * @param simulationFlags a set of simulation flags used to estimate a fee. + * @return Field value representing estimated fee. + */ + fun estimateFeeV3(call: Call, simulationFlags: Set): Request> + + /** + * Estimate fee for a call as a version 1 invoke transaction. + * + * Estimate fee for a signed call on starknet for specified block tag. + * + * @param call a call used to estimate a fee. + * @param blockTag a tag of the block in respect to what the query will be made. + * @return Field value representing estimated fee. + */ + fun estimateFee(call: Call, blockTag: BlockTag): Request> + + /** + * Estimate fee for a call as a version 3 invoke transaction. + * + * Estimate fee for a signed call on starknet for specified block tag. + * + * @param call a call used to estimate a fee. + * @param blockTag a tag of the block in respect to what the query will be made. + * @return Field value representing estimated fee. + */ + fun estimateFeeV3(call: Call, blockTag: BlockTag): Request> /** * Estimate fee for a call. @@ -221,14 +412,25 @@ interface Account { * * @param call a call used to estimate a fee. * @param blockTag a tag of the block in respect to what the query will be made. + * @param simulationFlags a set of simulation flags used to estimate a fee. * @return Field value representing estimated fee. */ - fun estimateFee(call: Call, blockTag: BlockTag): Request> { - return estimateFee(listOf(call), blockTag) - } + fun estimateFee(call: Call, blockTag: BlockTag, simulationFlags: Set): Request> /** - * Estimate fee for a list of calls. + * Estimate fee for a call as a version 3 invoke transaction. + * + * Estimate fee for a signed call on starknet for specified block tag. + * + * @param call a call used to estimate a fee. + * @param blockTag a tag of the block in respect to what the query will be made. + * @param simulationFlags a set of simulation flags used to estimate a fee. + * @return Field value representing estimated fee. + */ + fun estimateFeeV3(call: Call, blockTag: BlockTag, simulationFlags: Set): Request> + + /** + * Estimate fee for a list of calls as a version 1 invoke transaction. * * Estimate fee for a signed list of calls on starknet. * @@ -238,7 +440,39 @@ interface Account { fun estimateFee(calls: List): Request> /** - * Estimate fee for a list of calls. + * Estimate fee for a list of calls as a version 3 invoke transaction. + * + * Estimate fee for a signed list of calls on starknet. + * + * @param calls a list of calls used to estimate a fee. + * @return estimated fee as field value. + */ + fun estimateFeeV3(calls: List): Request> + + /** + * Estimate fee for a list of calls as a version 1 invoke transaction. + * + * Estimate fee for a signed list of calls on starknet. + * + * @param calls a list of calls used to estimate a fee. + * @param simulationFlags a set of simulation flags used to estimate a fee. + * @return estimated fee as field value. + */ + fun estimateFee(calls: List, simulationFlags: Set): Request> + + /** + * Estimate fee for a list of calls as a version 3 invoke transaction. + * + * Estimate fee for a signed list of calls on starknet. + * + * @param calls a list of calls used to estimate a fee. + * @param simulationFlags a set of simulation flags used to estimate a fee. + * @return estimated fee as field value. + */ + fun estimateFeeV3(calls: List, simulationFlags: Set): Request> + + /** + * Estimate fee for a list of calls as a version 1 invoke transaction. * * Estimate fee for a signed list of calls on starknet. * @@ -248,6 +482,49 @@ interface Account { */ fun estimateFee(calls: List, blockTag: BlockTag): Request> + /** + * Estimate fee for a list of calls. + * + * Estimate fee for a signed list of calls on starknet. + * + * @param calls a list of calls used to estimate a fee. + * @param blockTag a tag of the block in respect to what the query will be made. + * @return estimated fee as field value. + */ + fun estimateFeeV3(calls: List, blockTag: BlockTag): Request> + + /** + * Estimate fee for a list of calls using version 3 invoke transaction. + * + * Estimate fee for a signed list of calls on starknet. + * + * @param calls a list of calls used to estimate a fee. + * @param blockTag a tag of the block in respect to what the query will be made. + * @param simulationFlags a set of simulation flags used to estimate a fee. + * @return estimated fee as field value. + */ + fun estimateFee( + calls: List, + blockTag: BlockTag, + simulationFlags: Set, + ): Request> + + /** + * Estimate fee for a list of calls. + * + * Estimate fee for a signed list of calls on starknet. + * + * @param calls a list of calls used to estimate a fee. + * @param blockTag a tag of the block in respect to what the query will be made. + * @param simulationFlags a set of simulation flags used to estimate a fee. + * @return estimated fee as field value. + */ + fun estimateFeeV3( + calls: List, + blockTag: BlockTag, + simulationFlags: Set, + ): Request> + /** * Get account nonce. * diff --git a/lib/src/main/kotlin/com/swmansion/starknet/account/EstimatedFeeToMaxFee.kt b/lib/src/main/kotlin/com/swmansion/starknet/account/EstimatedFeeToMaxFee.kt deleted file mode 100644 index eb3f341b4..000000000 --- a/lib/src/main/kotlin/com/swmansion/starknet/account/EstimatedFeeToMaxFee.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.swmansion.starknet.account - -import com.swmansion.starknet.data.types.Felt -import java.math.BigInteger -import kotlin.math.roundToInt - -@JvmSynthetic -internal fun estimatedFeeToMaxFee(fee: Felt, overhead: Double = 0.1): Felt { - val multiplier = ((1 + overhead) * 100).roundToInt().toBigInteger() - val result = fee.value.multiply(multiplier).divide(BigInteger.valueOf(100)) - - return Felt(result) -} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/account/StandardAccount.kt b/lib/src/main/kotlin/com/swmansion/starknet/account/StandardAccount.kt index 0b93d0f90..110b448b0 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/account/StandardAccount.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/account/StandardAccount.kt @@ -3,8 +3,9 @@ package com.swmansion.starknet.account import com.swmansion.starknet.data.TypedData import com.swmansion.starknet.data.types.* import com.swmansion.starknet.data.types.transactions.* -import com.swmansion.starknet.data.types.transactions.DeployAccountTransactionPayload +import com.swmansion.starknet.data.types.transactions.DeployAccountTransactionV1Payload import com.swmansion.starknet.extensions.compose +import com.swmansion.starknet.extensions.toFelt import com.swmansion.starknet.provider.Provider import com.swmansion.starknet.provider.Request import com.swmansion.starknet.provider.exceptions.RequestFailedException @@ -26,8 +27,14 @@ class StandardAccount( private val provider: Provider, private val cairoVersion: Felt = Felt.ZERO, ) : Account { - private val version = Felt.ONE - private val estimateVersion: BigInteger = BigInteger.valueOf(2).pow(128).add(version.value) + private fun estimateVersion(version: Felt): Felt { + return BigInteger.valueOf(2).pow(128) + .add(version.value) + .toFelt + } + private val defaultFeeEstimateSimulationFlags: Set by lazy { + setOf(SimulationFlagForEstimateFee.SKIP_VALIDATE) + } /** * @param provider a provider used to interact with Starknet @@ -41,13 +48,13 @@ class StandardAccount( cairoVersion, ) - override fun sign(calls: List, params: ExecutionParams, forFeeEstimate: Boolean): InvokeTransactionPayload { + override fun sign(calls: List, params: ExecutionParams, forFeeEstimate: Boolean): InvokeTransactionV1Payload { val calldata = AccountCalldataTransformer.callsToExecuteCalldata(calls, cairoVersion) val signVersion = when (forFeeEstimate) { - true -> Felt(estimateVersion) - false -> version + true -> estimateVersion(Felt.ONE) + false -> Felt.ONE } - val tx = TransactionFactory.makeInvokeTransaction( + val tx = TransactionFactory.makeInvokeV1Transaction( senderAddress = address, calldata = calldata, chainId = provider.chainId, @@ -61,6 +68,31 @@ class StandardAccount( return signedTransaction.toPayload() } + override fun sign(calls: List, params: ExecutionParamsV3, forFeeEstimate: Boolean): InvokeTransactionV3Payload { + val calldata = AccountCalldataTransformer.callsToExecuteCalldata(calls, cairoVersion) + val signVersion = when (forFeeEstimate) { + true -> estimateVersion(Felt(3)) + false -> Felt(3) + } + val tx = TransactionFactory.makeInvokeV3Transaction( + senderAddress = address, + calldata = calldata, + chainId = provider.chainId, + nonce = params.nonce, + version = signVersion, + resourceBounds = params.resourceBounds, + tip = params.tip, + paymasterData = params.paymasterData, + accountDeploymentData = params.accountDeploymentData, + nonceDataAvailabilityMode = params.nonceDataAvailabilityMode, + feeDataAvailabilityMode = params.feeDataAvailabilityMode, + ) + + val signedTransaction = tx.copy(signature = signer.signTransaction(tx)) + + return signedTransaction.toPayload() + } + override fun signDeployAccount( classHash: Felt, calldata: Calldata, @@ -68,12 +100,12 @@ class StandardAccount( maxFee: Felt, nonce: Felt, forFeeEstimate: Boolean, - ): DeployAccountTransactionPayload { + ): DeployAccountTransactionV1Payload { val signVersion = when (forFeeEstimate) { - true -> Felt(estimateVersion) - false -> version + true -> estimateVersion(Felt.ONE) + false -> Felt.ONE } - val tx = TransactionFactory.makeDeployAccountTransaction( + val tx = TransactionFactory.makeDeployAccountV1Transaction( classHash = classHash, contractAddress = address, salt = salt, @@ -88,6 +120,36 @@ class StandardAccount( return signedTransaction.toPayload() } + override fun signDeployAccount( + classHash: Felt, + calldata: Calldata, + salt: Felt, + params: DeployAccountParamsV3, + forFeeEstimate: Boolean, + ): DeployAccountTransactionV3Payload { + val signVersion = when (forFeeEstimate) { + true -> estimateVersion(Felt(3)) + false -> Felt(3) + } + val tx = TransactionFactory.makeDeployAccountV3Transaction( + classHash = classHash, + senderAddress = address, + salt = salt, + calldata = calldata, + chainId = provider.chainId, + version = signVersion, + nonce = params.nonce, + resourceBounds = params.resourceBounds, + tip = params.tip, + paymasterData = params.paymasterData, + nonceDataAvailabilityMode = params.nonceDataAvailabilityMode, + feeDataAvailabilityMode = params.feeDataAvailabilityMode, + ) + val signedTransaction = tx.copy(signature = signer.signTransaction(tx)) + + return signedTransaction.toPayload() + } + override fun signDeclare( contractDefinition: Cairo0ContractDefinition, classHash: Felt, @@ -95,8 +157,8 @@ class StandardAccount( forFeeEstimate: Boolean, ): DeclareTransactionV1Payload { val signVersion = when (forFeeEstimate) { - true -> Felt(estimateVersion) - false -> version + true -> estimateVersion(Felt.ONE) + false -> Felt.ONE } val tx = TransactionFactory.makeDeclareV1Transaction( contractDefinition = contractDefinition, @@ -119,7 +181,7 @@ class StandardAccount( forFeeEstimate: Boolean, ): DeclareTransactionV2Payload { val signVersion = when (forFeeEstimate) { - true -> Felt(estimateVersion + BigInteger.valueOf(1)) + true -> estimateVersion(Felt(2)) false -> Felt(2) } val tx = TransactionFactory.makeDeclareV2Transaction( @@ -136,6 +198,35 @@ class StandardAccount( return signedTransaction.toPayload() } + override fun signDeclare( + sierraContractDefinition: Cairo1ContractDefinition, + casmContractDefinition: CasmContractDefinition, + params: DeclareParamsV3, + forFeeEstimate: Boolean, + ): DeclareTransactionV3Payload { + val signVersion = when (forFeeEstimate) { + true -> estimateVersion(Felt(3)) + false -> Felt(3) + } + val tx = TransactionFactory.makeDeclareV3Transaction( + contractDefinition = sierraContractDefinition, + senderAddress = address, + chainId = provider.chainId, + nonce = params.nonce, + version = signVersion, + resourceBounds = params.resourceBounds, + tip = params.tip, + paymasterData = params.paymasterData, + accountDeploymentData = params.accountDeploymentData, + casmContractDefinition = casmContractDefinition, + nonceDataAvailabilityMode = params.nonceDataAvailabilityMode, + feeDataAvailabilityMode = params.feeDataAvailabilityMode, + ) + val signedTransaction = tx.copy(signature = signer.signTransaction(tx)) + + return signedTransaction.toPayload() + } + override fun signTypedData(typedData: TypedData): Signature { return signer.signTypedData(typedData, address) } @@ -194,13 +285,48 @@ class StandardAccount( } } + override fun executeV3(calls: List, l1ResourceBounds: ResourceBounds): Request { + return getNonce().compose { nonce -> + val signParams = ExecutionParamsV3( + nonce = nonce, + l1ResourceBounds = l1ResourceBounds, + ) + val payload = sign(calls, signParams, false) + + return@compose provider.invokeFunction(payload) + } + } + override fun execute(calls: List): Request { return estimateFee(calls).compose { estimateFee -> - val maxFee = estimatedFeeToMaxFee(estimateFee.first().overallFee) + val maxFee = estimateFee.first().toMaxFee() execute(calls, maxFee) } } + override fun executeV3(calls: List): Request { + return estimateFee(calls).compose { estimateFee -> + val resourceBounds = estimateFee.first().toResourceBounds() + executeV3(calls, resourceBounds.l1Gas) + } + } + + override fun execute(call: Call, maxFee: Felt): Request { + return execute(listOf(call), maxFee) + } + + override fun executeV3(call: Call, l1ResourceBounds: ResourceBounds): Request { + return executeV3(listOf(call), l1ResourceBounds) + } + + override fun execute(call: Call): Request { + return execute(listOf(call)) + } + + override fun executeV3(call: Call): Request { + return executeV3(listOf(call)) + } + override fun getNonce(): Request = getNonce(BlockTag.PENDING) override fun getNonce(blockTag: BlockTag) = provider.getNonce(address, blockTag) @@ -209,14 +335,94 @@ class StandardAccount( override fun getNonce(blockNumber: Int) = provider.getNonce(address, blockNumber) + override fun estimateFee(call: Call): Request> { + return estimateFee(listOf(call)) + } + + override fun estimateFee(call: Call, simulationFlags: Set): Request> { + return estimateFee(listOf(call), simulationFlags) + } + + override fun estimateFee(call: Call, blockTag: BlockTag): Request> { + return estimateFee(listOf(call), blockTag) + } + + override fun estimateFee( + call: Call, + blockTag: BlockTag, + simulationFlags: Set, + ): Request> { + return estimateFee(listOf(call), blockTag, simulationFlags) + } + override fun estimateFee(calls: List): Request> { return estimateFee(calls, BlockTag.PENDING) } + override fun estimateFeeV3(calls: List): Request> { + return estimateFeeV3(calls, BlockTag.PENDING, defaultFeeEstimateSimulationFlags) + } + + override fun estimateFee( + calls: List, + simulationFlags: Set, + ): Request> { + return estimateFee(calls, BlockTag.PENDING, simulationFlags) + } + + override fun estimateFeeV3( + calls: List, + simulationFlags: Set, + ): Request> { + return estimateFeeV3(calls, BlockTag.PENDING, simulationFlags) + } + + override fun estimateFeeV3(calls: List, blockTag: BlockTag): Request> { + return estimateFeeV3(calls, blockTag) + } + override fun estimateFee(calls: List, blockTag: BlockTag): Request> { + return estimateFee(calls, blockTag, defaultFeeEstimateSimulationFlags) + } + + override fun estimateFee( + calls: List, + blockTag: BlockTag, + simulationFlags: Set, + ): Request> { return getNonce(blockTag).compose { nonce -> val payload = buildEstimateFeePayload(calls, nonce) - return@compose provider.getEstimateFee(payload, blockTag) + return@compose provider.getEstimateFee(payload, blockTag, simulationFlags) + } + } + + override fun estimateFeeV3( + call: Call, + simulationFlags: Set, + ): Request> { + return estimateFeeV3(listOf(call), simulationFlags) + } + + override fun estimateFeeV3(call: Call, blockTag: BlockTag): Request> { + return estimateFeeV3(listOf(call), blockTag) + } + + override fun estimateFeeV3( + call: Call, + blockTag: BlockTag, + simulationFlags: Set, + ): Request> { + return estimateFeeV3(listOf(call), blockTag, simulationFlags) + } + + override fun estimateFeeV3( + calls: List, + blockTag: BlockTag, + simulationFlags: Set, + ): Request> { + return getNonce(blockTag).compose { nonce -> + val payload = buildEstimateFeeV3Payload(calls, nonce) + return@compose provider.getEstimateFee(payload, blockTag, simulationFlags) } } @@ -224,7 +430,7 @@ class StandardAccount( val executionParams = ExecutionParams(nonce = nonce, maxFee = Felt.ZERO) val payload = sign(calls, executionParams, true) - val signedTransaction = TransactionFactory.makeInvokeTransaction( + val signedTransaction = TransactionFactory.makeInvokeV1Transaction( senderAddress = payload.senderAddress, calldata = payload.calldata, chainId = provider.chainId, @@ -235,4 +441,28 @@ class StandardAccount( ) return listOf(signedTransaction.toPayload()) } + + private fun buildEstimateFeeV3Payload(calls: List, nonce: Felt): List { + val executionParams = ExecutionParamsV3( + nonce = nonce, + l1ResourceBounds = ResourceBounds.ZERO, + ) + val payload = sign(calls, executionParams, false) + + val signedTransaction = TransactionFactory.makeInvokeV3Transaction( + senderAddress = payload.senderAddress, + calldata = payload.calldata, + chainId = provider.chainId, + nonce = nonce, + signature = payload.signature, + version = payload.version, + resourceBounds = payload.resourceBounds, + tip = payload.tip, + paymasterData = payload.paymasterData, + accountDeploymentData = payload.accountDeploymentData, + nonceDataAvailabilityMode = payload.nonceDataAvailabilityMode, + feeDataAvailabilityMode = payload.feeDataAvailabilityMode, + ) + return listOf(signedTransaction.toPayload()) + } } diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TransactionHashCalculator.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TransactionHashCalculator.kt index 35c35b589..f2d2c1950 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TransactionHashCalculator.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TransactionHashCalculator.kt @@ -1,27 +1,21 @@ package com.swmansion.starknet.data +import com.swmansion.starknet.crypto.Poseidon import com.swmansion.starknet.crypto.StarknetCurve -import com.swmansion.starknet.data.types.Calldata -import com.swmansion.starknet.data.types.Felt -import com.swmansion.starknet.data.types.StarknetChainId +import com.swmansion.starknet.data.types.* +import com.swmansion.starknet.data.types.transactions.DAMode import com.swmansion.starknet.data.types.transactions.TransactionType +import com.swmansion.starknet.extensions.toFelt /** * Toolkit for calculating hashes of transactions. */ object TransactionHashCalculator { - /** - * Calculate hash of invoke function transaction. - * - * @param contractAddress address of account that executes transaction - * @param calldata calldata sent to the account - * @param chainId id of the chain used - * @param version version of the tx - * @param nonce account's nonce - * @param maxFee maximum fee that account will use for the execution - */ + private val l1GasPrefix by lazy { Felt.fromShortString("L1_GAS") } + private val l2GasPrefix by lazy { Felt.fromShortString("L2_GAS") } + @JvmStatic - fun calculateInvokeTxHash( + fun calculateInvokeTxV1Hash( contractAddress: Felt, calldata: Calldata, chainId: StarknetChainId, @@ -39,18 +33,40 @@ object TransactionHashCalculator { nonce = nonce, ) - /** - * Calculate hash of deploy account transaction. - * - * @param classHash hash of the contract code - * @param calldata constructor calldata used for deployment - * @param salt salt used to calculate address - * @param chainId id of the chain used - * @param version version of the tx - * @param maxFee maximum fee that account will use for the execution - */ @JvmStatic - fun calculateDeployAccountTxHash( + fun calculateInvokeTxV3Hash( + senderAddress: Felt, + calldata: Calldata, + chainId: StarknetChainId, + version: Felt, + nonce: Felt, + tip: Uint64, + resourceBounds: ResourceBoundsMapping, + paymasterData: PaymasterData, + accountDeploymentData: AccountDeploymentData, + feeDataAvailabilityMode: DAMode, + nonceDataAvailabilityMode: DAMode, + ): Felt { + return Poseidon.poseidonHash( + *CommonTransanctionV3Fields( + txType = TransactionType.INVOKE, + version = version, + address = senderAddress, + tip = tip, + resourceBounds = resourceBounds, + paymasterData = paymasterData, + chainId = chainId, + nonce = nonce, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ).toTypedArray(), + Poseidon.poseidonHash(accountDeploymentData), + Poseidon.poseidonHash(calldata), + ) + } + + @JvmStatic + fun calculateDeployAccountV1TxHash( classHash: Felt, calldata: Calldata, salt: Felt, @@ -76,6 +92,44 @@ object TransactionHashCalculator { ) } + @JvmStatic + fun calculateDeployAccountV3TxHash( + classHash: Felt, + constructorCalldata: Calldata, + salt: Felt, + paymasterData: PaymasterData, + chainId: StarknetChainId, + version: Felt, + nonce: Felt, + tip: Uint64, + resourceBounds: ResourceBoundsMapping, + feeDataAvailabilityMode: DAMode, + nonceDataAvailabilityMode: DAMode, + ): Felt { + val contractAddress = ContractAddressCalculator.calculateAddressFromHash( + classHash = classHash, + calldata = constructorCalldata, + salt = salt, + ) + return Poseidon.poseidonHash( + *CommonTransanctionV3Fields( + txType = TransactionType.DEPLOY_ACCOUNT, + version = version, + address = contractAddress, + tip = tip, + resourceBounds = resourceBounds, + paymasterData = paymasterData, + chainId = chainId, + nonce = nonce, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ).toTypedArray(), + Poseidon.poseidonHash(constructorCalldata), + classHash, + salt, + ) + } + @JvmStatic fun calculateDeclareV1TxHash( classHash: Felt, @@ -122,6 +176,40 @@ object TransactionHashCalculator { ) } + @JvmStatic + fun calculateDeclareV3TxHash( + classHash: Felt, + chainId: StarknetChainId, + senderAddress: Felt, + version: Felt, + nonce: Felt, + compiledClassHash: Felt, + tip: Uint64, + resourceBounds: ResourceBoundsMapping, + paymasterData: PaymasterData, + accountDeploymentData: AccountDeploymentData, + feeDataAvailabilityMode: DAMode, + nonceDataAvailabilityMode: DAMode, + ): Felt { + return Poseidon.poseidonHash( + *CommonTransanctionV3Fields( + txType = TransactionType.DECLARE, + version = version, + address = senderAddress, + tip = tip, + resourceBounds = resourceBounds, + paymasterData = paymasterData, + chainId = chainId, + nonce = nonce, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ).toTypedArray(), + Poseidon.poseidonHash(accountDeploymentData), + classHash, + compiledClassHash, + ) + } + private fun transactionHashCommon( txType: TransactionType, version: Felt, @@ -143,4 +231,56 @@ object TransactionHashCalculator { nonce, ) } + + private fun CommonTransanctionV3Fields( + txType: TransactionType, + version: Felt, + address: Felt, + tip: Uint64, + resourceBounds: ResourceBoundsMapping, + paymasterData: PaymasterData, + chainId: StarknetChainId, + nonce: Felt, + nonceDataAvailabilityMode: DAMode, + feeDataAvailabilityMode: DAMode, + ): List { + return listOf( + txType.txPrefix, + version, + address, + Poseidon.poseidonHash( + tip.toFelt, + *resourceBoundsForFee(resourceBounds).toList().toTypedArray(), + ), + Poseidon.poseidonHash(paymasterData), + chainId.value.toFelt, + nonce, + dataAvailabilityModes( + feeDataAvailabilityMode, + nonceDataAvailabilityMode, + ), + ) + } + + private fun resourceBoundsForFee(resourceBounds: ResourceBoundsMapping): Pair { + val l1GasBound = l1GasPrefix.value.shiftLeft(64 + 128) + .add(resourceBounds.l1Gas.maxAmount.value.shiftLeft(128)) + .add(resourceBounds.l1Gas.maxPricePerUnit.value) + .toFelt + val l2GasBound = l2GasPrefix.value.shiftLeft(64 + 128) + .add(resourceBounds.l2Gas.maxAmount.value.shiftLeft(128)) + .add(resourceBounds.l2Gas.maxPricePerUnit.value) + .toFelt + + return l1GasBound to l2GasBound + } + + private fun dataAvailabilityModes( + feeDataAvailabilityMode: DAMode, + nonceDataAvailabilityMode: DAMode, + ): Felt { + return nonceDataAvailabilityMode.value.toBigInteger().shiftLeft(32) + .add(feeDataAvailabilityMode.value.toBigInteger()) + .toFelt + } } diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/DeprecatedCairoEntryPointSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/DeprecatedCairoEntryPointSerializer.kt index cc3e2f6ec..489fa3e29 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/DeprecatedCairoEntryPointSerializer.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/DeprecatedCairoEntryPointSerializer.kt @@ -2,6 +2,7 @@ package com.swmansion.starknet.data.serializers import com.swmansion.starknet.data.types.DeprecatedCairoEntryPoint import com.swmansion.starknet.extensions.toFelt +import com.swmansion.starknet.extensions.toNumAsHex import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.PrimitiveKind @@ -10,7 +11,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* -import toNumAsHex internal object DeprecatedCairoEntryPointSerializer : KSerializer { override fun deserialize(decoder: Decoder): DeprecatedCairoEntryPoint { diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/EstimateTransactionFeePayloadSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/EstimateTransactionFeePayloadSerializer.kt deleted file mode 100644 index 3a8f5e110..000000000 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/EstimateTransactionFeePayloadSerializer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.swmansion.starknet.data.serializers - -import com.swmansion.starknet.data.types.EstimateTransactionFeePayload -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.* - -@OptIn(kotlinx.serialization.ExperimentalSerializationApi::class) -internal object EstimateTransactionFeePayloadSerializer : KSerializer { - - override val descriptor: SerialDescriptor - get() = EstimateTransactionFeePayload.serializer().descriptor - - override fun serialize(encoder: Encoder, value: EstimateTransactionFeePayload) { - require(encoder is JsonEncoder) - - val jsonObject = buildJsonObject { - putJsonArray("request") { - value.request.forEach { - add(Json.encodeToJsonElement(TransactionPayloadSerializer, it)) - } - } - put("block_id", value.blockId.toString()) - } - - encoder.encodeJsonElement(jsonObject) - } - - override fun deserialize(decoder: Decoder): EstimateTransactionFeePayload { - throw SerializationException("Class used for serialization only.") - } -} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/ExecutionResourcesSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/ExecutionResourcesSerializer.kt new file mode 100644 index 000000000..c07c9673c --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/ExecutionResourcesSerializer.kt @@ -0,0 +1,63 @@ +package com.swmansion.starknet.data.serializers + +import com.swmansion.starknet.data.types.transactions.ExecutionResources +import com.swmansion.starknet.extensions.toNumAsHex +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* + +// TODO: (#344) Remove this workaround serializer once ExecutionResources conform to the RPC spec on Pathfinder side. +internal object ExecutionResourcesSerializer : KSerializer { + override fun deserialize(decoder: Decoder): ExecutionResources { + // This accepts both integer and NUM_AS_HEX values, as a temporary workaround until this is fixed on Pathfinder side. + val input = decoder as? JsonDecoder ?: throw SerializationException("Expected JsonInput for ${decoder::class}") + + val jsonObject = input.decodeJsonElement().jsonObject + + // TODO (#344): This shoulde be a mandatory field, but it's not on Pathfinder side. + // val steps = getAsInt(jsonObject, "steps") ?: throw SerializationException("Input element does not contain mandatory field 'steps'") + val steps = getAsInt(jsonObject, "steps") ?: 0 + val memoryHoles = getAsInt(jsonObject, "memory_holes") + val rangeCheckApplications = getAsInt(jsonObject, "range_check_builtin_applications") + val pedersenApplications = getAsInt(jsonObject, "pedersen_builtin_applications") + val poseidonApplications = getAsInt(jsonObject, "poseidon_builtin_applications") + val ecOpApplications = getAsInt(jsonObject, "ec_op_builtin_applications") + val ecdsaApplications = getAsInt(jsonObject, "ecdsa_builtin_applications") + val bitwiseApplications = getAsInt(jsonObject, "bitwise_builtin_applications") + val keccakApplications = getAsInt(jsonObject, "keccak_builtin_applications") + val segmentArenaApplications = getAsInt(jsonObject, "segment_arena_builtin") + + return ExecutionResources( + steps = steps, + memoryHoles = memoryHoles, + rangeCheckApplications = rangeCheckApplications, + pedersenApplications = pedersenApplications, + poseidonApplications = poseidonApplications, + ecOpApplications = ecOpApplications, + ecdsaApplications = ecdsaApplications, + bitwiseApplications = bitwiseApplications, + keccakApplications = keccakApplications, + segmentArenaApplications = segmentArenaApplications, + ) + } + + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("ExecutionResources", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: ExecutionResources) { + throw SerializationException("Class used for deserialization only.") + } + + private fun getAsInt(jsonObject: JsonObject, key: String): Int? { + return jsonObject.getOrDefault(key, null)?.jsonPrimitive?.contentOrNull?.let { fromHexOrInt(it) } + } + + private fun fromHexOrInt(value: String): Int { + return value.toBigIntegerOrNull()?.toInt() ?: value.toNumAsHex.value.toInt() + } +} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/FeltSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/FeltSerializer.kt deleted file mode 100644 index f321560bd..000000000 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/FeltSerializer.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.swmansion.starknet.data.serializers - -import com.swmansion.starknet.data.types.Felt -import com.swmansion.starknet.extensions.toHex -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -internal object FeltSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Felt", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): Felt { - val hex = decoder.decodeString() - - return Felt.fromHex(hex) - } - - override fun serialize(encoder: Encoder, value: Felt) { - encoder.encodeString(value.value.toHex()) - } -} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/NumAsHexSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/NumAsHexSerializer.kt deleted file mode 100644 index b6a9d1adc..000000000 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/NumAsHexSerializer.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.swmansion.starknet.data.serializers - -import com.swmansion.starknet.data.types.NumAsHex -import com.swmansion.starknet.extensions.toHex -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -internal object NumAsHexSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("NumAsHex", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): NumAsHex { - val hex = decoder.decodeString() - - return NumAsHex.fromHex(hex) - } - - override fun serialize(encoder: Encoder, value: NumAsHex) { - encoder.encodeString(value.value.toHex()) - } -} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/NumAsHexSerializers.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/NumAsHexSerializers.kt new file mode 100644 index 000000000..f47dd3d3a --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/NumAsHexSerializers.kt @@ -0,0 +1,48 @@ +package com.swmansion.starknet.data.serializers + +import com.swmansion.starknet.data.types.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal object NumAsHexSerializer : HexSerializer( + NumAsHex::fromHex, + NumAsHex::hexString, + PrimitiveSerialDescriptor("NumAsHex", PrimitiveKind.STRING), +) + +internal object FeltSerializer : HexSerializer( + Felt::fromHex, + Felt::hexString, + PrimitiveSerialDescriptor("Felt", PrimitiveKind.STRING), +) + +internal object Uint64Serializer : HexSerializer( + Uint64::fromHex, + Uint64::hexString, + PrimitiveSerialDescriptor("Uint64", PrimitiveKind.STRING), +) + +internal object Uint128Serializer : HexSerializer( + Uint128::fromHex, + Uint128::hexString, + PrimitiveSerialDescriptor("Uint128", PrimitiveKind.STRING), +) + +internal sealed class HexSerializer( + private val fromHex: (String) -> T, + private val hexString: (T) -> String, + override val descriptor: SerialDescriptor, +) : KSerializer { + override fun deserialize(decoder: Decoder): T { + val hex = decoder.decodeString() + return fromHex(hex) + } + + override fun serialize(encoder: Encoder, value: T) { + encoder.encodeString(hexString(value)) + } +} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TransactionPayloadSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TransactionPayloadSerializer.kt index b1ac0866e..11e69de62 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TransactionPayloadSerializer.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TransactionPayloadSerializer.kt @@ -4,21 +4,16 @@ import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.transactions.* import com.swmansion.starknet.extensions.add import com.swmansion.starknet.extensions.put -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* -@OptIn(ExperimentalSerializationApi::class) -@Serializer(forClass = TransactionPayload::class) internal object TransactionPayloadSerializer : KSerializer { - - override val descriptor: SerialDescriptor - get() = TransactionPayload.serializer().descriptor + override val descriptor = PrimitiveSerialDescriptor("TransactionPayload", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): TransactionPayload { require(decoder is JsonDecoder) @@ -29,43 +24,70 @@ internal object TransactionPayloadSerializer : KSerializer { val type = decoder.json.decodeFromJsonElement(TransactionType.serializer(), typeElement) return when (type) { - TransactionType.INVOKE -> decoder.json.decodeFromJsonElement(InvokeTransactionPayload.serializer(), element) + TransactionType.INVOKE -> deserializeInvoke(decoder, element) TransactionType.DECLARE -> deserializeDeclare(decoder, element) - TransactionType.DEPLOY_ACCOUNT -> decoder.json.decodeFromJsonElement(DeployAccountTransactionPayload.serializer(), element) + TransactionType.DEPLOY_ACCOUNT -> deserializeDeployAccount(decoder, element) else -> throw IllegalArgumentException("Invalid transaction type '${typeElement.jsonPrimitive.content}'") } } + override fun serialize(encoder: Encoder, value: TransactionPayload) { + require(encoder is JsonEncoder) + + val jsonObject = when (value) { + is InvokeTransactionV1Payload -> Json.encodeToJsonElement(InvokeTransactionV1Payload.serializer(), value).jsonObject + is InvokeTransactionV3Payload -> Json.encodeToJsonElement(InvokeTransactionV3Payload.serializer(), value).jsonObject + is DeclareTransactionV1Payload -> Json.encodeToJsonElement(DeclareTransactionV1PayloadSerializer, value).jsonObject + is DeclareTransactionV2Payload -> Json.encodeToJsonElement(DeclareTransactionV2PayloadSerializer, value).jsonObject + is DeclareTransactionV3Payload -> Json.encodeToJsonElement(DeclareTransactionV3PayloadSerializer, value).jsonObject + is DeployAccountTransactionV1Payload -> Json.encodeToJsonElement(DeployAccountTransactionV1Payload.serializer(), value).jsonObject + is DeployAccountTransactionV3Payload -> Json.encodeToJsonElement(DeployAccountTransactionV3Payload.serializer(), value).jsonObject + } + + val result = JsonObject( + jsonObject.plus("type" to Json.encodeToJsonElement(value.type)), + ) + + encoder.encodeJsonElement(result) + } + + private fun deserializeInvoke(decoder: JsonDecoder, element: JsonElement): InvokeTransactionPayload { + val versionElement = element.jsonObject.getOrElse("version") { throw SerializationException("Input element does not contain mandatory field 'version'") } + + val version = decoder.json.decodeFromJsonElement(Felt.serializer(), versionElement) + return when (version) { + Felt(3) -> decoder.json.decodeFromJsonElement(InvokeTransactionV3Payload.serializer(), element) + Felt.ONE -> decoder.json.decodeFromJsonElement(InvokeTransactionV1Payload.serializer(), element) + else -> throw IllegalArgumentException("Invalid invoke transaction version '${versionElement.jsonPrimitive.content}'") + } + } + private fun deserializeDeclare(decoder: JsonDecoder, element: JsonElement): DeclareTransactionPayload { val versionElement = element.jsonObject.getOrElse("version") { throw SerializationException("Input element does not contain mandatory field 'version'") } val version = decoder.json.decodeFromJsonElement(Felt.serializer(), versionElement) return when (version) { - Felt.ONE -> decoder.json.decodeFromJsonElement(DeclareTransactionV1Payload.serializer(), element) + Felt(3) -> decoder.json.decodeFromJsonElement(DeclareTransactionV3Payload.serializer(), element) Felt(2) -> decoder.json.decodeFromJsonElement(DeclareTransactionV2Payload.serializer(), element) + Felt.ONE -> decoder.json.decodeFromJsonElement(DeclareTransactionV1Payload.serializer(), element) else -> throw IllegalArgumentException("Invalid declare transaction version '${versionElement.jsonPrimitive.content}'") } } - override fun serialize(encoder: Encoder, value: TransactionPayload) { - require(encoder is JsonEncoder) + private fun deserializeDeployAccount(decoder: JsonDecoder, element: JsonElement): DeployAccountTransactionPayload { + val versionElement = element.jsonObject.getOrElse("version") { throw SerializationException("Input element does not contain mandatory field 'version'") } - val jsonObject = when (value) { - is InvokeTransactionPayload -> Json.encodeToJsonElement(InvokeTransactionPayloadSerializer, value).jsonObject - is DeclareTransactionV1Payload -> Json.encodeToJsonElement(DeclareTransactionV1PayloadSerializer, value).jsonObject - is DeclareTransactionV2Payload -> Json.encodeToJsonElement(DeclareTransactionV2PayloadSerializer, value).jsonObject - is DeployAccountTransactionPayload -> Json.encodeToJsonElement(DeployAccountTransactionPayloadSerializer, value).jsonObject - else -> throw IllegalArgumentException("Invalid transaction payload type [${value.type}]") + val version = decoder.json.decodeFromJsonElement(Felt.serializer(), versionElement) + return when (version) { + Felt(3) -> decoder.json.decodeFromJsonElement(DeployAccountTransactionV3Payload.serializer(), element) + Felt.ONE -> decoder.json.decodeFromJsonElement(DeployAccountTransactionV1Payload.serializer(), element) + else -> throw IllegalArgumentException("Invalid deploy account transaction version '${versionElement.jsonPrimitive.content}'") } - - encoder.encodeJsonElement(jsonObject) } } internal object DeclareTransactionV1PayloadSerializer : KSerializer { - - override val descriptor: SerialDescriptor - get() = DeclareTransactionV1Payload.serializer().descriptor + override val descriptor = PrimitiveSerialDescriptor("DeclareTransactionV1Payload", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: DeclareTransactionV1Payload) { require(encoder is JsonEncoder) @@ -88,11 +110,8 @@ internal object DeclareTransactionV1PayloadSerializer : KSerializer { - - override val descriptor: SerialDescriptor - get() = DeclareTransactionV2Payload.serializer().descriptor +internal object DeclareTransactionV2PayloadSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("DeclareTransactionV2Payload", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: DeclareTransactionV2Payload) { require(encoder is JsonEncoder) @@ -116,54 +135,32 @@ object DeclareTransactionV2PayloadSerializer : KSerializer { +internal object DeclareTransactionV3PayloadSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("DeclareTransactionV3Payload", PrimitiveKind.STRING) - override val descriptor: SerialDescriptor - get() = DeclareTransactionV1Payload.serializer().descriptor - - override fun serialize(encoder: Encoder, value: InvokeTransactionPayload) { + override fun serialize(encoder: Encoder, value: DeclareTransactionV3Payload) { require(encoder is JsonEncoder) val jsonObject = buildJsonObject { + put("contract_class", value.contractDefinition.toJson()) put("sender_address", value.senderAddress.hexString()) - putJsonArray("calldata") { value.calldata.forEach { add(it) } } - putJsonArray("signature") { value.signature.forEach { add(it) } } - put("max_fee", value.maxFee.hexString()) - put("version", value.version) - put("nonce", value.nonce) - put("type", value.type.toString()) - } - - encoder.encodeJsonElement(jsonObject) - } - - override fun deserialize(decoder: Decoder): InvokeTransactionPayload { - throw SerializationException("Class used for serialization only.") - } -} - -internal object DeployAccountTransactionPayloadSerializer : KSerializer { - override val descriptor: SerialDescriptor - get() = DeclareTransactionV1Payload.serializer().descriptor - - override fun serialize(encoder: Encoder, value: DeployAccountTransactionPayload) { - require(encoder is JsonEncoder) - - val jsonObject = buildJsonObject { - put("class_hash", value.classHash.hexString()) - put("contract_address_salt", value.salt.hexString()) - putJsonArray("constructor_calldata") { value.constructorCalldata.forEach { add(it) } } put("version", value.version) - put("nonce", value.nonce) - put("max_fee", value.maxFee.hexString()) putJsonArray("signature") { value.signature.forEach { add(it) } } + put("nonce", value.nonce) put("type", value.type.toString()) + put("compiled_class_hash", value.compiledClassHash.hexString()) + put("resource_bounds", Json.encodeToJsonElement(value.resourceBounds)) + put("tip", value.tip.hexString()) + putJsonArray("paymaster_data") { value.paymasterData.forEach { add(it) } } + putJsonArray("account_deployment_data") { value.accountDeploymentData.forEach { add(it) } } + put("fee_data_availability_mode", value.feeDataAvailabilityMode.toString()) + put("nonce_data_availability_mode", value.nonceDataAvailabilityMode.toString()) } encoder.encodeJsonElement(jsonObject) } - override fun deserialize(decoder: Decoder): DeployAccountTransactionPayload { + override fun deserialize(decoder: Decoder): DeclareTransactionV3Payload { throw SerializationException("Class used for serialization only.") } } diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TransactionPolymorphicSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TransactionPolymorphicSerializer.kt index 9c4905b22..88cbeafc2 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TransactionPolymorphicSerializer.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TransactionPolymorphicSerializer.kt @@ -16,7 +16,7 @@ internal object TransactionPolymorphicSerializer : JsonContentPolymorphicSeriali return when (type) { TransactionType.INVOKE -> selectInvokeDeserializer(element) TransactionType.DECLARE -> selectDeclareDeserializer(element) - TransactionType.DEPLOY_ACCOUNT -> DeployAccountTransactionV1.serializer() + TransactionType.DEPLOY_ACCOUNT -> selectDeployAccountDeserializer(element) TransactionType.DEPLOY -> DeployTransaction.serializer() TransactionType.L1_HANDLER -> L1HandlerTransaction.serializer() } @@ -27,21 +27,35 @@ internal object TransactionPolymorphicSerializer : JsonContentPolymorphicSeriali val version = Json.decodeFromJsonElement(Felt.serializer(), versionElement) return when (version) { + Felt(3) -> InvokeTransactionV3.serializer() Felt.ONE -> InvokeTransactionV1.serializer() Felt.ZERO -> InvokeTransactionV0.serializer() else -> throw IllegalArgumentException("Invalid invoke transaction version '${versionElement.jsonPrimitive.content}'") } } + private fun selectDeployAccountDeserializer(element: JsonElement): DeserializationStrategy { + val jsonElement = element.jsonObject + val versionElement = jsonElement.getOrElse("version") { throw SerializationException("Input element does not contain mandatory field 'version'") } + + val version = Json.decodeFromJsonElement(Felt.serializer(), versionElement) + return when (version) { + Felt(3) -> DeployAccountTransactionV3.serializer() + Felt.ONE -> DeployAccountTransactionV1.serializer() + else -> throw IllegalArgumentException("Invalid deploy account transaction version '${versionElement.jsonPrimitive.content}'") + } + } + private fun selectDeclareDeserializer(element: JsonElement): DeserializationStrategy { val jsonElement = element.jsonObject val versionElement = jsonElement.getOrElse("version") { throw SerializationException("Input element does not contain mandatory field 'version'") } val version = Json.decodeFromJsonElement(Felt.serializer(), versionElement) return when (version) { - Felt.ZERO -> DeclareTransactionV0.serializer() - Felt.ONE -> DeclareTransactionV1.serializer() + Felt(3) -> DeclareTransactionV3.serializer() Felt(2) -> DeclareTransactionV2.serializer() + Felt.ONE -> DeclareTransactionV1.serializer() + Felt.ZERO -> DeclareTransactionV0.serializer() else -> throw IllegalArgumentException("Invalid declare transaction version '${versionElement.jsonPrimitive.content}'") } } diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Event.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Event.kt index 553560d1d..7981dbe7b 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Event.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Event.kt @@ -40,25 +40,24 @@ data class OrderedEvent( val data: List, ) -@OptIn(ExperimentalSerializationApi::class) @Serializable data class EmittedEvent( - @JsonNames("address", "from_address") + @SerialName("from_address") val address: Felt, - @JsonNames("keys") + @SerialName("keys") val keys: List, - @JsonNames("data") + @SerialName("data") val data: List, - @JsonNames("block_hash") - val blockHash: Felt, + @SerialName("block_hash") + val blockHash: Felt? = null, - @JsonNames("block_number") - val blockNumber: Int, + @SerialName("block_number") + val blockNumber: Int? = null, - @JsonNames("transaction_hash") + @SerialName("transaction_hash") val transactionHash: Felt, ) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Execution.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Execution.kt index e388b9216..1e1d25bfb 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Execution.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Execution.kt @@ -8,6 +8,8 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable typealias Calldata = List +typealias PaymasterData = List +typealias AccountDeploymentData = List typealias Signature = List typealias CallArguments = List @@ -91,11 +93,6 @@ data class Call( } } -data class ExecutionParams( - val nonce: Felt, - val maxFee: Felt, -) - object AccountCalldataTransformer { @JvmSynthetic private fun callsToExecuteCalldataCairo1(calls: List): List { @@ -135,7 +132,7 @@ object AccountCalldataTransformer { } @JvmStatic - public fun callsToExecuteCalldata(calls: List, cairoVersion: Felt = Felt.ZERO): List { + fun callsToExecuteCalldata(calls: List, cairoVersion: Felt = Felt.ZERO): List { if (cairoVersion == Felt.ONE) { return callsToExecuteCalldataCairo1(calls) } diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt index c0d206de6..164d831af 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt @@ -21,25 +21,13 @@ data class Felt(override val value: BigInteger) : NumAsHexBase(value), Convertib } } - override fun toCalldata(): List = listOf(this) + override fun toCalldata() = listOf(this) - override fun toString(): String { - return "Felt(${value.toHex()})" - } + override fun toString() = "Felt(${value.toHex()})" - /** - * Encode as hexadecimal string, including "0x" prefix. - */ - override fun hexString(): String { - return value.toHex() - } + override fun hexString() = value.toHex() - /** - * Encode as decimal string. - */ - override fun decString(): String { - return value.toString(10) - } + override fun decString(): String = value.toString(10) /** * Encode as ASCII string, with up to 31 characters. @@ -61,6 +49,9 @@ data class Felt(override val value: BigInteger) : NumAsHexBase(value), Convertib @field:JvmField val PRIME = BigInteger("800000000000011000000000000000000000000000000000000000000000001", 16) + @field:JvmField + val MAX = PRIME.minus(BigInteger.ONE) + @field:JvmField val ZERO = Felt(BigInteger.ZERO) @@ -73,7 +64,9 @@ data class Felt(override val value: BigInteger) : NumAsHexBase(value), Convertib * @param value hex string. */ @JvmStatic - fun fromHex(value: String): Felt = Felt(parseHex(value)) + fun fromHex(value: String): Felt { + return Felt(parseHex(value)) + } /** * Create Felt from ASCII string. It must be shorter than 32 characters and only contain ASCII encoding. @@ -97,15 +90,6 @@ data class Felt(override val value: BigInteger) : NumAsHexBase(value), Convertib return fromHex("0x$encoded") } - /** - * Create Felt from NumAsHex. - * - * @param value NumAsHex to be transformed to felt. - * @throws IllegalArgumentException if value is negative or greater than Felt.PRIME. - */ - @JvmStatic - fun fromNumAsHex(value: NumAsHex): Felt = Felt(value.value) - private fun isAscii(string: String): Boolean { for (char in string) { if (char.code < 0 || char.code > 127) { diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/NumAsHex.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/NumAsHex.kt index e42b6b841..34641778e 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/NumAsHex.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/NumAsHex.kt @@ -6,15 +6,6 @@ import com.swmansion.starknet.extensions.toHex import kotlinx.serialization.Serializable import java.math.BigInteger -sealed class NumAsHexBase(open val value: BigInteger) : Comparable { - override fun compareTo(other: NumAsHexBase): Int { - return value.compareTo(other.value) - } - - abstract fun hexString(): String - abstract fun decString(): String -} - @Serializable(with = NumAsHexSerializer::class) data class NumAsHex(override val value: BigInteger) : NumAsHexBase(value) { constructor(value: Long) : this(BigInteger.valueOf(value)) @@ -26,23 +17,11 @@ data class NumAsHex(override val value: BigInteger) : NumAsHexBase(value) { } } - override fun toString(): String { - return "NumAsHex(${value.toHex()})" - } + override fun toString() = "NumAsHex(${value.toHex()})" - /** - * Encode as hexadecimal string, including "0x" prefix. - */ - override fun hexString(): String { - return value.toHex() - } + override fun hexString() = value.toHex() - /** - * Encode as decimal string. - */ - override fun decString(): String { - return value.toString(10) - } + override fun decString(): String = value.toString(10) companion object { @field:JvmField @@ -58,13 +37,5 @@ data class NumAsHex(override val value: BigInteger) : NumAsHexBase(value) { */ @JvmStatic fun fromHex(value: String): NumAsHex = NumAsHex(parseHex(value)) - - /** - * Create NumAsHex from Felt. - * - * @param value Felt. - */ - @JvmStatic - fun fromFelt(value: Felt): NumAsHex = NumAsHex(value.value) } } diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/NumAsHexBase.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/NumAsHexBase.kt new file mode 100644 index 000000000..b01493f3e --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/NumAsHexBase.kt @@ -0,0 +1,19 @@ +package com.swmansion.starknet.data.types + +import java.math.BigInteger + +sealed class NumAsHexBase(open val value: BigInteger) : Comparable { + override fun compareTo(other: NumAsHexBase): Int { + return value.compareTo(other.value) + } + + /** + * Encode as hexadecimal string, including "0x" prefix. + */ + abstract fun hexString(): String + + /** + * Encode as decimal string. + */ + abstract fun decString(): String +} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Params.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Params.kt new file mode 100644 index 000000000..69986bc71 --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Params.kt @@ -0,0 +1,121 @@ +@file:JvmName("Params") + +package com.swmansion.starknet.data.types + +import com.swmansion.starknet.data.types.transactions.DAMode + +/** + * Params used for sign and sending transactions. + */ + +sealed class ParamsBase + +/** + * Params used for signing and sending v1 and v2 transactions. + */ +data class ExecutionParams( + val nonce: Felt, + val maxFee: Felt, +) : ParamsBase() + +sealed class ParamsV3 : ParamsBase() { + abstract val nonce: Felt + abstract val resourceBounds: ResourceBoundsMapping + abstract val tip: Uint64 + abstract val paymasterData: PaymasterData + abstract val nonceDataAvailabilityMode: DAMode + abstract val feeDataAvailabilityMode: DAMode +} + +/** + * Params used for signing and sending v3 invoke transactions. + */ +// TODO: Make primary constructor public once values are no longer hardcoded on Starknet +@Suppress("DataClassPrivateConstructor") +data class ExecutionParamsV3 private constructor( + override val nonce: Felt, + override val resourceBounds: ResourceBoundsMapping, + override val tip: Uint64, + override val paymasterData: PaymasterData, + val accountDeploymentData: AccountDeploymentData, + override val nonceDataAvailabilityMode: DAMode, + override val feeDataAvailabilityMode: DAMode, +) : ParamsV3() { + constructor(nonce: Felt, l1ResourceBounds: ResourceBounds) : this( + nonce = nonce, + resourceBounds = ResourceBoundsMapping( + l1Gas = l1ResourceBounds, + l2Gas = ResourceBounds(Uint64.ZERO, Uint128.ZERO), + ), + tip = Uint64.ZERO, + paymasterData = emptyList(), + accountDeploymentData = emptyList(), + nonceDataAvailabilityMode = DAMode.L1, + feeDataAvailabilityMode = DAMode.L1, + ) +} + +/** + * Params used for signing and sending v3 declare transactions. + */ +// TODO: Make primary constructor public once values are no longer hardcoded on Starknet +@Suppress("DataClassPrivateConstructor") +data class DeclareParamsV3 private constructor( + override val nonce: Felt, + override val resourceBounds: ResourceBoundsMapping, + override val tip: Uint64, + override val paymasterData: PaymasterData, + val accountDeploymentData: AccountDeploymentData, + override val nonceDataAvailabilityMode: DAMode, + override val feeDataAvailabilityMode: DAMode, +) : ParamsV3() { + constructor(nonce: Felt, l1ResourceBounds: ResourceBounds) : this( + nonce = nonce, + resourceBounds = ResourceBoundsMapping( + l1Gas = l1ResourceBounds, + l2Gas = ResourceBounds(Uint64.ZERO, Uint128.ZERO), + ), + tip = Uint64.ZERO, + paymasterData = emptyList(), + accountDeploymentData = emptyList(), + nonceDataAvailabilityMode = DAMode.L1, + feeDataAvailabilityMode = DAMode.L1, + ) +} + +/** + * Params used for signing and sending v3 deploy account transactions. + */ +// TODO: Make primary constructor public once values are no longer hardcoded on Starknet +@Suppress("DataClassPrivateConstructor") +data class DeployAccountParamsV3 private constructor( + override val nonce: Felt, + override val resourceBounds: ResourceBoundsMapping, + override val tip: Uint64, + override val paymasterData: PaymasterData, + override val nonceDataAvailabilityMode: DAMode, + override val feeDataAvailabilityMode: DAMode, +) : ParamsV3() { + constructor( + nonce: Felt = Felt.ZERO, + resourceBounds: ResourceBoundsMapping, + ) : this( + nonce = nonce, + resourceBounds = resourceBounds, + tip = Uint64.ZERO, + paymasterData = emptyList(), + nonceDataAvailabilityMode = DAMode.L1, + feeDataAvailabilityMode = DAMode.L1, + ) + + constructor( + nonce: Felt = Felt.ZERO, + l1ResourceBounds: ResourceBounds, + ) : this( + nonce = nonce, + resourceBounds = ResourceBoundsMapping( + l1Gas = l1ResourceBounds, + l2Gas = ResourceBounds.ZERO, + ), + ) +} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Payloads.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Payloads.kt index b85ce3330..39c2463df 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Payloads.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Payloads.kt @@ -1,6 +1,5 @@ package com.swmansion.starknet.data.types -import com.swmansion.starknet.data.serializers.TransactionPayloadSerializer import com.swmansion.starknet.data.types.transactions.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -52,10 +51,10 @@ data class GetTransactionStatusPayload( @Serializable data class EstimateTransactionFeePayload( @SerialName("request") - val request: List< - @Serializable(with = TransactionPayloadSerializer::class) - TransactionPayload, - >, + val request: List, + + @SerialName("simulation_flags") + val simulationFlags: Set, @SerialName("block_id") override val blockId: BlockId, @@ -116,7 +115,6 @@ data class GetTransactionByBlockIdAndIndexPayload( data class SimulateTransactionsPayload( @SerialName("transactions") val transactions: List< - @Serializable(with = TransactionPayloadSerializer::class) TransactionPayload, >, diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Responses.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Responses.kt index e126b5cd2..64cca1f01 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Responses.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Responses.kt @@ -5,10 +5,13 @@ import com.swmansion.starknet.data.serializers.TransactionPolymorphicSerializer import com.swmansion.starknet.data.types.transactions.Transaction import com.swmansion.starknet.data.types.transactions.TransactionExecutionStatus import com.swmansion.starknet.data.types.transactions.TransactionStatus +import com.swmansion.starknet.extensions.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +import java.math.BigInteger +import kotlin.math.roundToInt @Serializable data class CallContractResponse( @@ -41,18 +44,44 @@ data class DeployAccountResponse( val address: Felt? = null, ) -@OptIn(ExperimentalSerializationApi::class) @Serializable data class EstimateFeeResponse( - @JsonNames("gas_consumed", "gas_usage") + @SerialName("gas_consumed") val gasConsumed: Felt, - @JsonNames("gas_price") + @SerialName("gas_price") val gasPrice: Felt, - @JsonNames("overall_fee") + @SerialName("overall_fee") val overallFee: Felt, -) + + // TODO: (#344) Deviation from the spec, make this non-nullable once Pathfinder is updated + @SerialName("unit") + val feeUnit: PriceUnit? = null, +) { + fun toMaxFee(overhead: Double = 0.5): Felt { + return addOverhead(overallFee.value, overhead).toFelt + } + + fun toResourceBounds( + amountOverhead: Double = 0.1, + unitPriceOverhead: Double = 0.5, + ): ResourceBoundsMapping { + val maxAmount = addOverhead(gasConsumed.value, amountOverhead).toUint64 + val maxPricePerUnit = addOverhead(gasPrice.value, unitPriceOverhead).toUint128 + + // As of Starknet 0.13.0, the L2 gas is not supported + // Because of this, the L2 gas values are hardcoded to 0 + return ResourceBoundsMapping( + l1Gas = ResourceBounds(maxAmount = maxAmount, maxPricePerUnit = maxPricePerUnit), + l2Gas = ResourceBounds(maxAmount = Uint64.ZERO, maxPricePerUnit = Uint128.ZERO), + ) + } + private fun addOverhead(value: BigInteger, overhead: Double): BigInteger { + val multiplier = ((1 + overhead) * 100).roundToInt().toBigInteger() + return value.multiply(multiplier).divide(BigInteger.valueOf(100)) + } +} @OptIn(ExperimentalSerializationApi::class) @Serializable @@ -396,11 +425,65 @@ data class PendingStateUpdateResponse( override val stateDiff: StateDiff, ) : StateUpdate() +// TODO: remove SCREAMING_SNAKE_CASE @JsonNames once devnet is updated +@OptIn(ExperimentalSerializationApi::class) +@Serializable +data class ResourceBoundsMapping( + @SerialName("l1_gas") + @JsonNames("L1_GAS") + val l1Gas: ResourceBounds, + + @SerialName("l2_gas") + @JsonNames("L2_GAS") + val l2Gas: ResourceBounds, +) + +@Serializable +data class ResourceBounds( + @SerialName("max_amount") + val maxAmount: Uint64, + + @SerialName("max_price_per_unit") + val maxPricePerUnit: Uint128, +) { + companion object { + @field:JvmField + val ZERO = ResourceBounds(Uint64.ZERO, Uint128.ZERO) + } + + fun toMaxFee(): Felt { + return maxAmount.value.multiply(maxPricePerUnit.value).toFelt + } +} + +@OptIn(ExperimentalSerializationApi::class) @Serializable data class ResourcePrice( + // TODO: (#344) This is a deviation from the spec, make this non-nullable once Juno is updated @SerialName("price_in_wei") - val priceInWei: NumAsHex, + val priceInWei: Felt? = null, + + @SerialName("price_in_fri") + @JsonNames("price_in_strk") // TODO: (#344) RPC 0.5.0 legacy name, remove once Pathfinder is updated + val priceInFri: Felt, +) + +@Serializable +data class FeePayment( + @SerialName("amount") + val amount: Felt, - @SerialName("price_in_strk") - val priceInStark: NumAsHex? = null, + @SerialName("unit") + val unit: PriceUnit, ) + +@OptIn(ExperimentalSerializationApi::class) +@Serializable +enum class PriceUnit { + @SerialName("WEI") + WEI, + + @SerialName("FRI") + @JsonNames("STRK") // TODO: (#344) RPC 0.5.0 legacy name, remove once Pathfinder is updated + FRI, +} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint128.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint128.kt new file mode 100644 index 000000000..fc7b96027 --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint128.kt @@ -0,0 +1,53 @@ +package com.swmansion.starknet.data.types + +import com.swmansion.starknet.data.parseHex +import com.swmansion.starknet.data.serializers.Uint128Serializer +import com.swmansion.starknet.data.types.conversions.ConvertibleToCalldata +import com.swmansion.starknet.extensions.toFelt +import com.swmansion.starknet.extensions.toHex +import kotlinx.serialization.Serializable +import java.math.BigInteger + +@Serializable(with = Uint128Serializer::class) +data class Uint128(override val value: BigInteger) : NumAsHexBase(value), ConvertibleToCalldata { + constructor(value: Long) : this(BigInteger.valueOf(value)) + constructor(value: Int) : this(BigInteger.valueOf(value.toLong())) + + init { + if (value < BigInteger.ZERO) { + throw IllegalArgumentException("Default Uint128 constructor does not accept negative numbers, [$value] given.") + } + if (value >= MAX) { + throw IllegalArgumentException("Default Uint128 constructor does not accept numbers higher than 2^128-1, [$value] given.") + } + } + + override fun toCalldata() = listOf(this.value.toFelt) + + override fun toString() = "Uint128(${value.toHex()})" + + override fun hexString() = value.toHex() + + override fun decString(): String = value.toString(10) + + companion object { + @field:JvmField + val MAX = BigInteger.valueOf(2).pow(128).minus(BigInteger.ONE) + + @field:JvmField + val ZERO = Uint128(BigInteger.ZERO) + + @field:JvmField + val ONE = Uint128(BigInteger.ONE) + + /** + * Create Uint128 from hex string. It must start with "0x" prefix. + * + * @param value hex string. + */ + @JvmStatic + fun fromHex(value: String): Uint128 { + return Uint128(parseHex(value)) + } + } +} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint256.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint256.kt index 6f20f09dd..6b0fa168e 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint256.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint256.kt @@ -2,13 +2,13 @@ package com.swmansion.starknet.data.types import com.swmansion.starknet.data.parseHex import com.swmansion.starknet.data.types.conversions.ConvertibleToCalldata +import com.swmansion.starknet.extensions.toHex import java.math.BigInteger private const val SHIFT = 128 -private val MAX: BigInteger = BigInteger.valueOf(2).pow(256).minus(BigInteger.ONE) private val SHIFT_MOD: BigInteger = BigInteger.valueOf(2).pow(128) -data class Uint256(val value: BigInteger) : ConvertibleToCalldata { +data class Uint256(override val value: BigInteger) : NumAsHexBase(value), ConvertibleToCalldata { constructor(value: Long) : this(BigInteger.valueOf(value)) constructor(value: Int) : this(BigInteger.valueOf(value.toLong())) constructor(value: Felt) : this(value.value) @@ -38,11 +38,18 @@ data class Uint256(val value: BigInteger) : ConvertibleToCalldata { val high: Felt get() = Felt(value.shiftRight(SHIFT)) - override fun toCalldata(): List { - return listOf(low, high) - } + override fun toCalldata() = listOf(low, high) + + override fun toString() = "Uint256($value)" + + override fun hexString() = value.toHex() + + override fun decString(): String = value.toString(10) companion object { + @field:JvmField + val MAX = BigInteger.valueOf(2).pow(256).minus(BigInteger.ONE) + @field:JvmField val ZERO = Uint256(BigInteger.ZERO) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint64.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint64.kt new file mode 100644 index 000000000..7538d5ba3 --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Uint64.kt @@ -0,0 +1,53 @@ +package com.swmansion.starknet.data.types + +import com.swmansion.starknet.data.parseHex +import com.swmansion.starknet.data.serializers.Uint64Serializer +import com.swmansion.starknet.data.types.conversions.ConvertibleToCalldata +import com.swmansion.starknet.extensions.toFelt +import com.swmansion.starknet.extensions.toHex +import kotlinx.serialization.Serializable +import java.math.BigInteger + +@Serializable(with = Uint64Serializer::class) +data class Uint64(override val value: BigInteger) : NumAsHexBase(value), ConvertibleToCalldata { + constructor(value: Long) : this(BigInteger.valueOf(value)) + constructor(value: Int) : this(BigInteger.valueOf(value.toLong())) + + init { + if (value < BigInteger.ZERO) { + throw IllegalArgumentException("Default Uint64 constructor does not accept negative numbers, [$value] given.") + } + if (value >= MAX) { + throw IllegalArgumentException("Default Uint64 constructor does not accept numbers higher than 2^64-1, [$value] given.") + } + } + + override fun toCalldata() = listOf(this.value.toFelt) + + override fun toString() = "Uint64(${value.toHex()})" + + override fun hexString() = value.toHex() + + override fun decString(): String = value.toString(10) + + companion object { + @field:JvmField + val MAX = BigInteger.valueOf(2).pow(64).minus(BigInteger.ONE) + + @field:JvmField + val ZERO = Uint64(BigInteger.ZERO) + + @field:JvmField + val ONE = Uint64(BigInteger.ONE) + + /** + * Create Uint64 from hex string. It must start with "0x" prefix. + * + * @param value hex string. + */ + @JvmStatic + fun fromHex(value: String): Uint64 { + return Uint64(parseHex(value)) + } + } +} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/ExecutionResources.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/ExecutionResources.kt index a19cc281b..e99e2f278 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/ExecutionResources.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/ExecutionResources.kt @@ -1,35 +1,38 @@ package com.swmansion.starknet.data.types.transactions -import com.swmansion.starknet.data.types.NumAsHex +import com.swmansion.starknet.data.serializers.ExecutionResourcesSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -@Serializable +@Serializable(with = ExecutionResourcesSerializer::class) data class ExecutionResources( @SerialName("steps") - val steps: NumAsHex, + val steps: Int, @SerialName("memory_holes") - val memoryHoles: NumAsHex? = null, + val memoryHoles: Int? = null, @SerialName("range_check_builtin_applications") - val rangeCheckApplications: NumAsHex, + val rangeCheckApplications: Int? = null, @SerialName("pedersen_builtin_applications") - val pedersenApplications: NumAsHex, + val pedersenApplications: Int? = null, @SerialName("poseidon_builtin_applications") - val poseidonApplications: NumAsHex, + val poseidonApplications: Int? = null, @SerialName("ec_op_builtin_applications") - val ecOpApplications: NumAsHex, + val ecOpApplications: Int? = null, @SerialName("ecdsa_builtin_applications") - val ecdsaApplications: NumAsHex, + val ecdsaApplications: Int? = null, @SerialName("bitwise_builtin_applications") - val bitwiseApplications: NumAsHex, + val bitwiseApplications: Int? = null, @SerialName("keccak_builtin_applications") - val keccakApplications: NumAsHex, + val keccakApplications: Int? = null, + + @SerialName("segment_arena_builtin") + val segmentArenaApplications: Int? = null, ) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/SimulatedTransaction.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/SimulatedTransaction.kt index 01790b861..a1e6aa7f1 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/SimulatedTransaction.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/SimulatedTransaction.kt @@ -30,6 +30,11 @@ enum class SimulationFlag(val value: String) { SKIP_FEE_CHARGE("SKIP_FEE_CHARGE"), } +@Serializable +enum class SimulationFlagForEstimateFee(val value: String) { + SKIP_VALIDATE("SKIP_VALIDATE"), +} + @Serializable data class FunctionInvocation( @SerialName("contract_address") @@ -64,6 +69,9 @@ data class FunctionInvocation( @SerialName("messages") val messages: List, + + @SerialName("execution_resources") + val executionResources: ExecutionResources, ) @Serializable diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/Transaction.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/Transaction.kt index f2fa1eca3..a3c57d70f 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/Transaction.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/Transaction.kt @@ -8,11 +8,6 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames -typealias DeployAccountTransaction = DeployAccountTransactionV1 - -@JvmSynthetic -internal val INVOKE_VERSION = Felt.ONE - @OptIn(ExperimentalSerializationApi::class) @Serializable enum class TransactionType(val txPrefix: Felt) { @@ -33,16 +28,48 @@ enum class TransactionType(val txPrefix: Felt) { L1_HANDLER(Felt.fromHex("0x6c315f68616e646c6572")), // encodeShortString('l1_handler') } +@Serializable +enum class DAMode(val value: Int) { + @SerialName("L1") + L1(0), + + @SerialName("L2") + L2(1), +} + @Serializable sealed class Transaction { abstract val hash: Felt? - abstract val maxFee: Felt abstract val version: Felt abstract val signature: Signature abstract val nonce: Felt abstract val type: TransactionType } +// @Serializable +// data class TransactionV1 { +// @SerialName("max_fee") +// val maxFee: Felt +// } + +@Serializable +sealed interface TransactionV3 { + @SerialName("resource_bounds") + val resourceBounds: ResourceBoundsMapping + + @SerialName("tip") + val tip: Uint64 + + @SerialName("paymaster_data") + val paymasterData: List + + @SerialName("nonce_data_availability_mode") + val nonceDataAvailabilityMode: DAMode + + @SerialName("fee_data_availability_mode") + val feeDataAvailabilityMode: DAMode +} + @Serializable @SerialName("DEPLOY") data class DeployTransaction( @@ -61,7 +88,7 @@ data class DeployTransaction( // not in RPC spec @SerialName("max_fee") - override val maxFee: Felt = Felt.ZERO, + val maxFee: Felt = Felt.ZERO, @SerialName("version") override val version: Felt, @@ -97,10 +124,10 @@ data class InvokeTransactionV1( override val hash: Felt? = null, @SerialName("max_fee") - override val maxFee: Felt, + val maxFee: Felt, @SerialName("version") - override val version: Felt = INVOKE_VERSION, + override val version: Felt = Felt.ONE, @SerialName("signature") override val signature: Signature, @@ -109,8 +136,8 @@ data class InvokeTransactionV1( override val nonce: Felt, ) : InvokeTransaction() { - fun toPayload(): InvokeTransactionPayload { - return InvokeTransactionPayload( + fun toPayload(): InvokeTransactionV1Payload { + return InvokeTransactionV1Payload( calldata = calldata, signature = signature, maxFee = maxFee, @@ -122,7 +149,7 @@ data class InvokeTransactionV1( companion object { @JvmStatic - internal fun fromPayload(payload: InvokeTransactionPayload): InvokeTransaction { + internal fun fromPayload(payload: InvokeTransactionV1Payload): InvokeTransaction { return InvokeTransactionV1( senderAddress = payload.senderAddress, calldata = payload.calldata, @@ -136,6 +163,82 @@ data class InvokeTransactionV1( } } +@Serializable +data class InvokeTransactionV3( + @SerialName("calldata") + override val calldata: Calldata, + + @SerialName("sender_address") + val senderAddress: Felt, + + // not in RPC spec, but returned alongside the transaction + @SerialName("transaction_hash") + override val hash: Felt? = null, + + @SerialName("version") + override val version: Felt = Felt(3), + + @SerialName("signature") + override val signature: Signature, + + @SerialName("nonce") + override val nonce: Felt, + + @SerialName("resource_bounds") + override val resourceBounds: ResourceBoundsMapping, + + @SerialName("tip") + override val tip: Uint64, + + @SerialName("paymaster_data") + override val paymasterData: List, + + @SerialName("account_deployment_data") + val accountDeploymentData: List, + + @SerialName("nonce_data_availability_mode") + override val nonceDataAvailabilityMode: DAMode, + + @SerialName("fee_data_availability_mode") + override val feeDataAvailabilityMode: DAMode, +) : InvokeTransaction(), TransactionV3 { + fun toPayload(): InvokeTransactionV3Payload { + return InvokeTransactionV3Payload( + calldata = calldata, + signature = signature, + nonce = nonce, + senderAddress = senderAddress, + version = version, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + accountDeploymentData = accountDeploymentData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ) + } + + companion object { + @JvmStatic + internal fun fromPayload(payload: InvokeTransactionV3Payload): InvokeTransactionV3 { + return InvokeTransactionV3( + senderAddress = payload.senderAddress, + calldata = payload.calldata, + hash = Felt.ZERO, + version = payload.version, + signature = payload.signature, + nonce = payload.nonce, + resourceBounds = payload.resourceBounds, + tip = payload.tip, + paymasterData = payload.paymasterData, + accountDeploymentData = payload.accountDeploymentData, + nonceDataAvailabilityMode = payload.nonceDataAvailabilityMode, + feeDataAvailabilityMode = payload.feeDataAvailabilityMode, + ) + } + } +} + @Serializable data class InvokeTransactionV0( @SerialName("calldata") @@ -146,7 +249,7 @@ data class InvokeTransactionV0( override val hash: Felt? = null, @SerialName("max_fee") - override val maxFee: Felt, + val maxFee: Felt, @SerialName("version") override val version: Felt = Felt.ZERO, @@ -186,7 +289,7 @@ data class DeclareTransactionV0( override val hash: Felt? = null, @SerialName("max_fee") - override val maxFee: Felt, + val maxFee: Felt, @SerialName("version") override val version: Felt = Felt.ZERO, @@ -214,7 +317,7 @@ data class DeclareTransactionV1( override val hash: Felt? = null, @SerialName("max_fee") - override val maxFee: Felt, + val maxFee: Felt, @SerialName("version") override val version: Felt = Felt.ONE, @@ -245,7 +348,6 @@ data class DeclareTransactionV1( } @Serializable -@SerialName("DECLARE") data class DeclareTransactionV2( @SerialName("class_hash") override val classHash: Felt, @@ -258,7 +360,7 @@ data class DeclareTransactionV2( override val hash: Felt? = null, @SerialName("max_fee") - override val maxFee: Felt, + val maxFee: Felt, @SerialName("version") override val version: Felt = Felt(2), @@ -291,6 +393,72 @@ data class DeclareTransactionV2( internal class ConvertingToPayloadFailedException : RuntimeException() } +@Serializable +data class DeclareTransactionV3( + @SerialName("class_hash") + override val classHash: Felt, + + @SerialName("sender_address") + override val senderAddress: Felt, + + // not in RPC spec + @SerialName("transaction_hash") + override val hash: Felt? = null, + + @SerialName("version") + override val version: Felt = Felt(3), + + @SerialName("signature") + override val signature: Signature, + + @SerialName("nonce") + override val nonce: Felt, + + @SerialName("resource_bounds") + override val resourceBounds: ResourceBoundsMapping, + + @SerialName("tip") + override val tip: Uint64, + + @SerialName("paymaster_data") + override val paymasterData: List, + + @SerialName("account_deployment_data") + val accountDeploymentData: List, + + @SerialName("nonce_data_availability_mode") + override val nonceDataAvailabilityMode: DAMode, + + @SerialName("fee_data_availability_mode") + override val feeDataAvailabilityMode: DAMode, + + @SerialName("compiled_class_hash") + val compiledClassHash: Felt, + + @SerialName("contract_class") + val contractDefinition: Cairo1ContractDefinition? = null, +) : DeclareTransaction(), TransactionV3 { + @Throws(ConvertingToPayloadFailedException::class) + internal fun toPayload(): DeclareTransactionV3Payload { + contractDefinition ?: throw ConvertingToPayloadFailedException() + return DeclareTransactionV3Payload( + contractDefinition = contractDefinition, + senderAddress = senderAddress, + nonce = nonce, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + accountDeploymentData = accountDeploymentData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + signature = signature, + compiledClassHash = compiledClassHash, + ) + } + + internal class ConvertingToPayloadFailedException : RuntimeException() +} + @Serializable @SerialName("L1_HANDLER") data class L1HandlerTransaction( @@ -308,7 +476,7 @@ data class L1HandlerTransaction( override val hash: Felt? = null, @SerialName("max_fee") - override val maxFee: Felt = Felt.ZERO, + val maxFee: Felt = Felt.ZERO, @SerialName("version") override val version: Felt, @@ -325,26 +493,44 @@ data class L1HandlerTransaction( @Serializable @SerialName("DEPLOY_ACCOUNT") +sealed class DeployAccountTransaction : Transaction() { + @SerialName("class_hash") + abstract val classHash: Felt + + @SerialName("contract_address") + abstract val contractAddress: Felt + + @SerialName("contract_address_salt") + abstract val contractAddressSalt: Felt + + @SerialName("constructor_calldata") + abstract val constructorCalldata: Calldata + + @SerialName("type") + override val type: TransactionType = TransactionType.DEPLOY_ACCOUNT +} + +@Serializable data class DeployAccountTransactionV1( @SerialName("class_hash") - val classHash: Felt, + override val classHash: Felt, // not in RPC spec, can be removed in the future @SerialName("contract_address") - val contractAddress: Felt = Felt.ZERO, + override val contractAddress: Felt = Felt.ZERO, @SerialName("contract_address_salt") - val contractAddressSalt: Felt, + override val contractAddressSalt: Felt, @SerialName("constructor_calldata") - val constructorCalldata: Calldata, + override val constructorCalldata: Calldata, // not in RPC spec, but returned alongside the transaction @SerialName("transaction_hash") override val hash: Felt? = null, @SerialName("max_fee") - override val maxFee: Felt, + val maxFee: Felt, @SerialName("version") override val version: Felt, @@ -354,12 +540,9 @@ data class DeployAccountTransactionV1( @SerialName("nonce") override val nonce: Felt, - - @SerialName("type") - override val type: TransactionType = TransactionType.DEPLOY_ACCOUNT, -) : Transaction() { - internal fun toPayload(): DeployAccountTransactionPayload { - return DeployAccountTransactionPayload( +) : DeployAccountTransaction() { + internal fun toPayload(): DeployAccountTransactionV1Payload { + return DeployAccountTransactionV1Payload( classHash = classHash, salt = contractAddressSalt, constructorCalldata = constructorCalldata, @@ -371,9 +554,70 @@ data class DeployAccountTransactionV1( } } +@Serializable +data class DeployAccountTransactionV3( + @SerialName("class_hash") + override val classHash: Felt, + + // not in RPC spec, can be removed in the future + @SerialName("contract_address") + override val contractAddress: Felt = Felt.ZERO, + + @SerialName("contract_address_salt") + override val contractAddressSalt: Felt, + + @SerialName("constructor_calldata") + override val constructorCalldata: Calldata, + + // not in RPC spec, but returned alongside the transaction + @SerialName("transaction_hash") + override val hash: Felt? = null, + + @SerialName("version") + override val version: Felt, + + @SerialName("signature") + override val signature: Signature, + + @SerialName("nonce") + override val nonce: Felt, + + @SerialName("resource_bounds") + override val resourceBounds: ResourceBoundsMapping, + + @SerialName("tip") + override val tip: Uint64, + + @SerialName("paymaster_data") + override val paymasterData: List, + + @SerialName("nonce_data_availability_mode") + override val nonceDataAvailabilityMode: DAMode, + + @SerialName("fee_data_availability_mode") + override val feeDataAvailabilityMode: DAMode, +) : DeployAccountTransaction(), TransactionV3 { + internal fun toPayload(): DeployAccountTransactionV3Payload { + return DeployAccountTransactionV3Payload( + classHash = classHash, + salt = contractAddressSalt, + constructorCalldata = constructorCalldata, + version = version, + nonce = nonce, + signature = signature, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + + ) + } +} + object TransactionFactory { @JvmStatic - fun makeInvokeTransaction( + fun makeInvokeV1Transaction( senderAddress: Felt, calldata: Calldata, chainId: StarknetChainId, @@ -382,7 +626,7 @@ object TransactionFactory { signature: Signature = emptyList(), version: Felt, ): InvokeTransactionV1 { - val hash = TransactionHashCalculator.calculateInvokeTxHash( + val hash = TransactionHashCalculator.calculateInvokeTxV1Hash( contractAddress = senderAddress, calldata = calldata, chainId = chainId, @@ -402,7 +646,51 @@ object TransactionFactory { } @JvmStatic - fun makeDeployAccountTransaction( + fun makeInvokeV3Transaction( + senderAddress: Felt, + calldata: Calldata, + chainId: StarknetChainId, + nonce: Felt, + signature: Signature = emptyList(), + version: Felt, + resourceBounds: ResourceBoundsMapping, + tip: Uint64, + paymasterData: List, + accountDeploymentData: List, + nonceDataAvailabilityMode: DAMode, + feeDataAvailabilityMode: DAMode, + ): InvokeTransactionV3 { + val hash = TransactionHashCalculator.calculateInvokeTxV3Hash( + senderAddress = senderAddress, + calldata = calldata, + chainId = chainId, + version = version, + nonce = nonce, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + accountDeploymentData = accountDeploymentData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ) + return InvokeTransactionV3( + hash = hash, + senderAddress = senderAddress, + calldata = calldata, + version = version, + signature = signature, + nonce = nonce, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + accountDeploymentData = accountDeploymentData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ) + } + + @JvmStatic + fun makeDeployAccountV1Transaction( classHash: Felt, contractAddress: Felt, salt: Felt, @@ -413,7 +701,7 @@ object TransactionFactory { signature: Signature = emptyList(), nonce: Felt = Felt.ZERO, ): DeployAccountTransactionV1 { - val hash = TransactionHashCalculator.calculateDeployAccountTxHash( + val hash = TransactionHashCalculator.calculateDeployAccountV1TxHash( classHash = classHash, calldata = calldata, salt = salt, @@ -435,6 +723,52 @@ object TransactionFactory { ) } + @JvmStatic + fun makeDeployAccountV3Transaction( + classHash: Felt, + senderAddress: Felt, + salt: Felt, + calldata: Calldata, + chainId: StarknetChainId, + version: Felt, + signature: Signature = emptyList(), + nonce: Felt = Felt.ZERO, + resourceBounds: ResourceBoundsMapping, + tip: Uint64, + paymasterData: List, + nonceDataAvailabilityMode: DAMode, + feeDataAvailabilityMode: DAMode, + ): DeployAccountTransactionV3 { + val hash = TransactionHashCalculator.calculateDeployAccountV3TxHash( + classHash = classHash, + salt = salt, + constructorCalldata = calldata, + chainId = chainId, + version = version, + nonce = nonce, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ) + return DeployAccountTransactionV3( + classHash = classHash, + contractAddress = senderAddress, + contractAddressSalt = salt, + constructorCalldata = calldata, + version = version, + nonce = nonce, + hash = hash, + signature = signature, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ) + } + @JvmStatic fun makeDeclareV1Transaction( classHash: Felt, @@ -500,4 +834,54 @@ object TransactionFactory { compiledClassHash = compiledClassHash, ) } + + @JvmStatic + fun makeDeclareV3Transaction( + senderAddress: Felt, + contractDefinition: Cairo1ContractDefinition, + chainId: StarknetChainId, + version: Felt, + nonce: Felt, + casmContractDefinition: CasmContractDefinition, + resourceBounds: ResourceBoundsMapping, + tip: Uint64, + paymasterData: List, + accountDeploymentData: List, + nonceDataAvailabilityMode: DAMode, + feeDataAvailabilityMode: DAMode, + signature: Signature = emptyList(), + ): DeclareTransactionV3 { + val classHash = Cairo1ClassHashCalculator.computeSierraClassHash(contractDefinition) + val compiledClassHash = Cairo1ClassHashCalculator.computeCasmClassHash(casmContractDefinition) + val hash = TransactionHashCalculator.calculateDeclareV3TxHash( + classHash = classHash, + chainId = chainId, + senderAddress = senderAddress, + version = version, + nonce = nonce, + compiledClassHash = compiledClassHash, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + accountDeploymentData = accountDeploymentData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ) + return DeclareTransactionV3( + hash = hash, + classHash = classHash, + senderAddress = senderAddress, + contractDefinition = contractDefinition, + version = version, + signature = signature, + nonce = nonce, + compiledClassHash = compiledClassHash, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + accountDeploymentData = accountDeploymentData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + ) + } } diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/TransactionPayload.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/TransactionPayload.kt index d1e7ceb7c..63a176438 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/TransactionPayload.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/TransactionPayload.kt @@ -1,17 +1,23 @@ package com.swmansion.starknet.data.types.transactions +import com.swmansion.starknet.data.serializers.TransactionPayloadSerializer import com.swmansion.starknet.data.types.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +@Serializable(with = TransactionPayloadSerializer::class) +sealed class TransactionPayload { + abstract val type: TransactionType +} + @Serializable -sealed class TransactionPayload() { +sealed class InvokeTransactionPayload : TransactionPayload() { @SerialName("type") - abstract val type: TransactionType + override val type: TransactionType = TransactionType.INVOKE } @Serializable -data class InvokeTransactionPayload( +data class InvokeTransactionV1Payload( @SerialName("sender_address") val senderAddress: Felt, @@ -29,19 +35,77 @@ data class InvokeTransactionPayload( @SerialName("nonce") val nonce: Felt, - - @SerialName("type") - override val type: TransactionType = TransactionType.INVOKE, -) : TransactionPayload() { +) : InvokeTransactionPayload() { constructor(senderAddress: Felt, calldata: Calldata, signature: Signature, maxFee: Felt, nonce: Felt) : this( - senderAddress, - calldata, - signature, - maxFee, - INVOKE_VERSION, - nonce, - TransactionType.INVOKE, + senderAddress = senderAddress, + calldata = calldata, + signature = signature, + maxFee = maxFee, + version = Felt.ONE, + nonce = nonce, + ) +} + +@Serializable +data class InvokeTransactionV3Payload( + @SerialName("sender_address") + val senderAddress: Felt, + + @SerialName("calldata") + val calldata: Calldata, + + @SerialName("signature") + val signature: Signature, + + @SerialName("nonce") + val nonce: Felt, + + @SerialName("resource_bounds") + val resourceBounds: ResourceBoundsMapping, + + @SerialName("tip") + val tip: Uint64, + + @SerialName("paymaster_data") + val paymasterData: List, + + @SerialName("account_deployment_data") + val accountDeploymentData: List, + + @SerialName("nonce_data_availability_mode") + val nonceDataAvailabilityMode: DAMode, + + @SerialName("fee_data_availability_mode") + val feeDataAvailabilityMode: DAMode, + + @SerialName("version") + val version: Felt, +) : InvokeTransactionPayload() { + + constructor( + senderAddress: Felt, + calldata: Calldata, + signature: Signature, + nonce: Felt, + resourceBounds: ResourceBoundsMapping, + tip: Uint64, + paymasterData: List, + accountDeploymentData: List, + nonceDataAvailabilityMode: DAMode, + feeDataAvailabilityMode: DAMode, + ) : this( + senderAddress = senderAddress, + calldata = calldata, + signature = signature, + nonce = nonce, + resourceBounds = resourceBounds, + tip = tip, + paymasterData = paymasterData, + accountDeploymentData = accountDeploymentData, + nonceDataAvailabilityMode = nonceDataAvailabilityMode, + feeDataAvailabilityMode = feeDataAvailabilityMode, + version = Felt(3), ) } @@ -100,7 +164,52 @@ data class DeclareTransactionV2Payload( ) : DeclareTransactionPayload() @Serializable -data class DeployAccountTransactionPayload( +data class DeclareTransactionV3Payload( + @SerialName("contract_class") + val contractDefinition: Cairo1ContractDefinition, + + @SerialName("nonce") + val nonce: Felt, + + @SerialName("signature") + val signature: Signature, + + @SerialName("sender_address") + val senderAddress: Felt, + + @SerialName("compiled_class_hash") + val compiledClassHash: Felt, + + @SerialName("resource_bounds") + val resourceBounds: ResourceBoundsMapping, + + @SerialName("tip") + val tip: Uint64, + + @SerialName("paymaster_data") + val paymasterData: List, + + @SerialName("account_deployment_data") + val accountDeploymentData: List, + + @SerialName("nonce_data_availability_mode") + val nonceDataAvailabilityMode: DAMode, + + @SerialName("fee_data_availability_mode") + val feeDataAvailabilityMode: DAMode, + + @SerialName("version") + val version: Felt = Felt(3), + + @SerialName("type") + override val type: TransactionType = TransactionType.DECLARE, +) : DeclareTransactionPayload() + +@Serializable +sealed class DeployAccountTransactionPayload() : TransactionPayload() + +@Serializable +data class DeployAccountTransactionV1Payload( @SerialName("class_hash") val classHash: Felt, @@ -124,4 +233,43 @@ data class DeployAccountTransactionPayload( @SerialName("type") override val type: TransactionType = TransactionType.DEPLOY_ACCOUNT, -) : TransactionPayload() +) : DeployAccountTransactionPayload() + +@Serializable +data class DeployAccountTransactionV3Payload( + @SerialName("class_hash") + val classHash: Felt, + + @SerialName("contract_address_salt") + val salt: Felt, + + @SerialName("constructor_calldata") + val constructorCalldata: Calldata, + + @SerialName("version") + val version: Felt, + + @SerialName("nonce") + val nonce: Felt, + + @SerialName("signature") + val signature: Signature, + + @SerialName("resource_bounds") + val resourceBounds: ResourceBoundsMapping, + + @SerialName("tip") + val tip: Uint64, + + @SerialName("paymaster_data") + val paymasterData: List, + + @SerialName("nonce_data_availability_mode") + val nonceDataAvailabilityMode: DAMode, + + @SerialName("fee_data_availability_mode") + val feeDataAvailabilityMode: DAMode, + + @SerialName("type") + override val type: TransactionType = TransactionType.DEPLOY_ACCOUNT, +) : DeployAccountTransactionPayload() diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/TransactionReceipt.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/TransactionReceipt.kt index c07dc623b..f974a17d0 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/TransactionReceipt.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/transactions/TransactionReceipt.kt @@ -1,10 +1,8 @@ package com.swmansion.starknet.data.types.transactions -import com.swmansion.starknet.data.types.Event -import com.swmansion.starknet.data.types.Felt -import com.swmansion.starknet.data.types.MessageL2ToL1 -import com.swmansion.starknet.data.types.NumAsHex +import com.swmansion.starknet.data.types.* import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames @@ -30,17 +28,16 @@ enum class TransactionStatus { REVERTED, } -@OptIn(ExperimentalSerializationApi::class) @Serializable enum class TransactionExecutionStatus { - @JsonNames("SUCCEEDED") + @SerialName("SUCCEEDED") SUCCEEDED, - @JsonNames("REVERTED") + @SerialName("REVERTED") REVERTED, // Not in RPC spec - @JsonNames("REJECTED") + @SerialName("REJECTED") REJECTED, } @@ -66,7 +63,7 @@ enum class TransactionFinalityStatus { sealed class TransactionReceipt { abstract val hash: Felt abstract val type: TransactionType - abstract val actualFee: Felt? + abstract val actualFee: FeePayment abstract val executionStatus: TransactionExecutionStatus abstract val finalityStatus: TransactionFinalityStatus abstract val revertReason: String? @@ -94,328 +91,316 @@ sealed class PendingTransactionReceipt : TransactionReceipt() { override val isPending: Boolean = true } -@OptIn(ExperimentalSerializationApi::class) @Serializable data class ProcessedInvokeTransactionReceipt( - @JsonNames("transaction_hash") + @SerialName("transaction_hash") override val hash: Felt, - @JsonNames("actual_fee") - override val actualFee: Felt, + @SerialName("actual_fee") + override val actualFee: FeePayment, - @JsonNames("execution_status") + @SerialName("execution_status") override val executionStatus: TransactionExecutionStatus, - @JsonNames("finality_status") + @SerialName("finality_status") override val finalityStatus: TransactionFinalityStatus, - @JsonNames("block_hash") + @SerialName("block_hash") override val blockHash: Felt, - @JsonNames("block_number") + @SerialName("block_number") override val blockNumber: Int, - @JsonNames("type") + @SerialName("type") override val type: TransactionType = TransactionType.INVOKE, - @JsonNames("messages_sent") + @SerialName("messages_sent") override val messagesSent: List, - @JsonNames("revert_reason") + @SerialName("revert_reason") override val revertReason: String? = null, - @JsonNames("events") + @SerialName("events") override val events: List, - @JsonNames("execution_resources") + @SerialName("execution_resources") override val executionResources: ExecutionResources, ) : ProcessedTransactionReceipt() -@OptIn(ExperimentalSerializationApi::class) @Serializable data class PendingInvokeTransactionReceipt( - @JsonNames("transaction_hash") + @SerialName("transaction_hash") override val hash: Felt, - @JsonNames("actual_fee") - override val actualFee: Felt, + @SerialName("actual_fee") + override val actualFee: FeePayment, - @JsonNames("messages_sent") + @SerialName("messages_sent") override val messagesSent: List, - @JsonNames("events") + @SerialName("events") override val events: List, - @JsonNames("type") + @SerialName("type") override val type: TransactionType = TransactionType.INVOKE, - @JsonNames("revert_reason") + @SerialName("revert_reason") override val revertReason: String? = null, - @JsonNames("finality_status") + @SerialName("finality_status") override val finalityStatus: TransactionFinalityStatus = TransactionFinalityStatus.ACCEPTED_ON_L2, - @JsonNames("execution_status") + @SerialName("execution_status") override val executionStatus: TransactionExecutionStatus, - @JsonNames("execution_resources") + @SerialName("execution_resources") override val executionResources: ExecutionResources, ) : PendingTransactionReceipt() -@OptIn(ExperimentalSerializationApi::class) @Serializable data class ProcessedDeclareTransactionReceipt( - @JsonNames("transaction_hash") + @SerialName("transaction_hash") override val hash: Felt, - @JsonNames("actual_fee") - override val actualFee: Felt, + @SerialName("actual_fee") + override val actualFee: FeePayment, - @JsonNames("execution_status") + @SerialName("execution_status") override val executionStatus: TransactionExecutionStatus, - @JsonNames("finality_status") + @SerialName("finality_status") override val finalityStatus: TransactionFinalityStatus, - @JsonNames("block_hash") + @SerialName("block_hash") override val blockHash: Felt, - @JsonNames("block_number") + @SerialName("block_number") override val blockNumber: Int, override val type: TransactionType = TransactionType.DECLARE, - @JsonNames("messages_sent") + @SerialName("messages_sent") override val messagesSent: List, - @JsonNames("revert_reason") + @SerialName("revert_reason") override val revertReason: String? = null, - @JsonNames("events") + @SerialName("events") override val events: List, - @JsonNames("execution_resources") + @SerialName("execution_resources") override val executionResources: ExecutionResources, ) : ProcessedTransactionReceipt() -@OptIn(ExperimentalSerializationApi::class) @Serializable data class PendingDeclareTransactionReceipt( - @JsonNames("transaction_hash") + @SerialName("transaction_hash") override val hash: Felt, - @JsonNames("actual_fee") - override val actualFee: Felt, + @SerialName("actual_fee") + override val actualFee: FeePayment, - @JsonNames("messages_sent") + @SerialName("messages_sent") override val messagesSent: List, - @JsonNames("events") + @SerialName("events") override val events: List, - @JsonNames("type") + @SerialName("type") override val type: TransactionType = TransactionType.DECLARE, - @JsonNames("revert_reason") + @SerialName("revert_reason") override val revertReason: String? = null, - @JsonNames("finality_status") + @SerialName("finality_status") override val finalityStatus: TransactionFinalityStatus = TransactionFinalityStatus.ACCEPTED_ON_L2, - @JsonNames("execution_status") + @SerialName("execution_status") override val executionStatus: TransactionExecutionStatus, - @JsonNames("execution_resources") + @SerialName("execution_resources") override val executionResources: ExecutionResources, - - @JsonNames("contract_address") - val contractAddress: Felt, ) : PendingTransactionReceipt() -@OptIn(ExperimentalSerializationApi::class) @Serializable data class ProcessedDeployAccountTransactionReceipt( - @JsonNames("transaction_hash") + @SerialName("transaction_hash") override val hash: Felt, - @JsonNames("actual_fee") - override val actualFee: Felt, + @SerialName("actual_fee") + override val actualFee: FeePayment, - @JsonNames("execution_status") + @SerialName("execution_status") override val executionStatus: TransactionExecutionStatus, - @JsonNames("finality_status") + @SerialName("finality_status") override val finalityStatus: TransactionFinalityStatus, - @JsonNames("block_hash") + @SerialName("block_hash") override val blockHash: Felt, - @JsonNames("block_number") + @SerialName("block_number") override val blockNumber: Int, - @JsonNames("type") + @SerialName("type") override val type: TransactionType = TransactionType.DEPLOY_ACCOUNT, - @JsonNames("messages_sent") + @SerialName("messages_sent") override val messagesSent: List, - @JsonNames("revert_reason") + @SerialName("revert_reason") override val revertReason: String? = null, - @JsonNames("events") + @SerialName("events") override val events: List, - @JsonNames("execution_resources") + @SerialName("execution_resources") override val executionResources: ExecutionResources, - @JsonNames("contract_address") + @SerialName("contract_address") val contractAddress: Felt, ) : ProcessedTransactionReceipt() -@OptIn(ExperimentalSerializationApi::class) @Serializable data class PendingDeployAccountTransactionReceipt( - @JsonNames("transaction_hash") + @SerialName("transaction_hash") override val hash: Felt, - @JsonNames("actual_fee") - override val actualFee: Felt, + @SerialName("actual_fee") + override val actualFee: FeePayment, - @JsonNames("messages_sent") + @SerialName("messages_sent") override val messagesSent: List, - @JsonNames("events") + @SerialName("events") override val events: List, - @JsonNames("type") + @SerialName("type") override val type: TransactionType = TransactionType.DEPLOY_ACCOUNT, - @JsonNames("revert_reason") + @SerialName("revert_reason") override val revertReason: String? = null, - @JsonNames("finality_status") + @SerialName("finality_status") override val finalityStatus: TransactionFinalityStatus = TransactionFinalityStatus.ACCEPTED_ON_L2, - @JsonNames("execution_status") + @SerialName("execution_status") override val executionStatus: TransactionExecutionStatus, - @JsonNames("execution_resources") + @SerialName("execution_resources") override val executionResources: ExecutionResources, - @JsonNames("contract_address") + @SerialName("contract_address") val contractAddress: Felt, ) : PendingTransactionReceipt() -@OptIn(ExperimentalSerializationApi::class) @Serializable data class ProcessedDeployTransactionReceipt( - @JsonNames("transaction_hash") + @SerialName("transaction_hash") override val hash: Felt, - @JsonNames("actual_fee") - override val actualFee: Felt, + @SerialName("actual_fee") + override val actualFee: FeePayment, - @JsonNames("execution_status") + @SerialName("execution_status") override val executionStatus: TransactionExecutionStatus, - @JsonNames("finality_status") + @SerialName("finality_status") override val finalityStatus: TransactionFinalityStatus, - @JsonNames("block_hash") + @SerialName("block_hash") override val blockHash: Felt, - @JsonNames("block_number") + @SerialName("block_number") override val blockNumber: Int, - @JsonNames("type") + @SerialName("type") override val type: TransactionType, - @JsonNames("messages_sent") + @SerialName("messages_sent") override val messagesSent: List, - @JsonNames("revert_reason") + @SerialName("revert_reason") override val revertReason: String? = null, - @JsonNames("events") + @SerialName("events") override val events: List, - @JsonNames("execution_resources") + @SerialName("execution_resources") override val executionResources: ExecutionResources, - @JsonNames("contract_address") + @SerialName("contract_address") val contractAddress: Felt, ) : ProcessedTransactionReceipt() -@OptIn(ExperimentalSerializationApi::class) @Serializable data class ProcessedL1HandlerTransactionReceipt( - @JsonNames("transaction_hash") + @SerialName("transaction_hash") override val hash: Felt, - @JsonNames("actual_fee") - override val actualFee: Felt, + @SerialName("actual_fee") + override val actualFee: FeePayment, - @JsonNames("execution_status") + @SerialName("execution_status") override val executionStatus: TransactionExecutionStatus, - @JsonNames("finality_status") + @SerialName("finality_status") override val finalityStatus: TransactionFinalityStatus, - @JsonNames("block_hash") + @SerialName("block_hash") override val blockHash: Felt, - @JsonNames("block_number") + @SerialName("block_number") override val blockNumber: Int, - @JsonNames("type") + @SerialName("type") override val type: TransactionType = TransactionType.L1_HANDLER, - @JsonNames("messages_sent") + @SerialName("messages_sent") override val messagesSent: List, - @JsonNames("revert_reason") + @SerialName("revert_reason") override val revertReason: String? = null, - @JsonNames("events") + @SerialName("events") override val events: List, - @JsonNames("execution_resources") + @SerialName("execution_resources") override val executionResources: ExecutionResources, - @JsonNames("message_hash") + @SerialName("message_hash") val messageHash: NumAsHex, ) : ProcessedTransactionReceipt() -@OptIn(ExperimentalSerializationApi::class) @Serializable data class PendingL1HandlerTransactionReceipt( - @JsonNames("transaction_hash") + @SerialName("transaction_hash") override val hash: Felt, - @JsonNames("actual_fee") - override val actualFee: Felt, + @SerialName("actual_fee") + override val actualFee: FeePayment, - @JsonNames("messages_sent") + @SerialName("messages_sent") override val messagesSent: List, - @JsonNames("events") + @SerialName("events") override val events: List, - @JsonNames("type") + @SerialName("type") override val type: TransactionType = TransactionType.L1_HANDLER, - @JsonNames("revert_reason") + @SerialName("revert_reason") override val revertReason: String? = null, - @JsonNames("finality_status") + @SerialName("finality_status") override val finalityStatus: TransactionFinalityStatus = TransactionFinalityStatus.ACCEPTED_ON_L2, - @JsonNames("execution_status") + @SerialName("execution_status") override val executionStatus: TransactionExecutionStatus, - @JsonNames("execution_resources") + @SerialName("execution_resources") override val executionResources: ExecutionResources, - @JsonNames("message_hash") + @SerialName("message_hash") val messageHash: Felt, ) : PendingTransactionReceipt() diff --git a/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToFelt.kt b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToFelt.kt index ce4642d1c..383944e5b 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToFelt.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToFelt.kt @@ -1,6 +1,7 @@ package com.swmansion.starknet.extensions import com.swmansion.starknet.data.types.Felt +import com.swmansion.starknet.data.types.NumAsHexBase import java.math.BigInteger @get:JvmSynthetic @@ -18,3 +19,7 @@ val Int.toFelt: Felt @get:JvmSynthetic val Long.toFelt: Felt get() = Felt(this) + +@get:JvmSynthetic +val NumAsHexBase.toFelt: Felt + get() = Felt(this.value) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToNumAsHex.kt b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToNumAsHex.kt index bf656a8e9..3569760ab 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToNumAsHex.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToNumAsHex.kt @@ -1,4 +1,7 @@ +package com.swmansion.starknet.extensions + import com.swmansion.starknet.data.types.NumAsHex +import com.swmansion.starknet.data.types.NumAsHexBase import java.math.BigInteger @get:JvmSynthetic @@ -8,3 +11,7 @@ val String.toNumAsHex: NumAsHex @get:JvmSynthetic val BigInteger.toNumAsHex: NumAsHex get() = NumAsHex(this) + +@get:JvmSynthetic +val NumAsHexBase.toNumAsHex: NumAsHex + get() = NumAsHex(this.value) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToUint128.kt b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToUint128.kt new file mode 100644 index 000000000..920b1116c --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToUint128.kt @@ -0,0 +1,25 @@ +package com.swmansion.starknet.extensions + +import com.swmansion.starknet.data.types.NumAsHexBase +import com.swmansion.starknet.data.types.Uint128 +import java.math.BigInteger + +@get:JvmSynthetic +val BigInteger.toUint128: Uint128 + get() = Uint128(this) + +@get:JvmSynthetic +val String.toUint128: Uint128 + get() = Uint128.fromHex(this) + +@get:JvmSynthetic +val Int.toUint128: Uint128 + get() = Uint128(this) + +@get:JvmSynthetic +val Long.toUint128: Uint128 + get() = Uint128(this) + +@get:JvmSynthetic +val NumAsHexBase.toUint128: Uint128 + get() = Uint128(this.value) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToUint256.kt b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToUint256.kt new file mode 100644 index 000000000..786a79f55 --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToUint256.kt @@ -0,0 +1,25 @@ +package com.swmansion.starknet.extensions + +import com.swmansion.starknet.data.types.NumAsHexBase +import com.swmansion.starknet.data.types.Uint256 +import java.math.BigInteger + +@get:JvmSynthetic +val BigInteger.toUint256: Uint256 + get() = Uint256(this) + +@get:JvmSynthetic +val String.toUint256: Uint256 + get() = Uint256.fromHex(this) + +@get:JvmSynthetic +val Int.toUint256: Uint256 + get() = Uint256(this) + +@get:JvmSynthetic +val Long.toUint256: Uint256 + get() = Uint256(this) + +@get:JvmSynthetic +val NumAsHexBase.toUint256: Uint256 + get() = Uint256(this.value) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToUint64.kt b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToUint64.kt new file mode 100644 index 000000000..a35061148 --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/extensions/ToUint64.kt @@ -0,0 +1,25 @@ +package com.swmansion.starknet.extensions + +import com.swmansion.starknet.data.types.NumAsHexBase +import com.swmansion.starknet.data.types.Uint64 +import java.math.BigInteger + +@get:JvmSynthetic +val BigInteger.toUint64: Uint64 + get() = Uint64(this) + +@get:JvmSynthetic +val String.toUint64: Uint64 + get() = Uint64.fromHex(this) + +@get:JvmSynthetic +val Int.toUint64: Uint64 + get() = Uint64(this) + +@get:JvmSynthetic +val Long.toUint64: Uint64 + get() = Uint64(this) + +@get:JvmSynthetic +val NumAsHexBase.toUint64: Uint64 + get() = Uint64(this.value) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/provider/Provider.kt b/lib/src/main/kotlin/com/swmansion/starknet/provider/Provider.kt index 560008f18..aacfb2b17 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/provider/Provider.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/provider/Provider.kt @@ -24,15 +24,26 @@ interface Provider { fun getTransaction(transactionHash: Felt): Request /** - * Invoke a function. + * Invoke a function using version 1 transaction. * - * Invoke a function in deployed contract. + * Invoke a function in deployed contract using version 1 transaction. * - * @param payload invoke function payload + * @param payload invoke function version 1 payload * * @throws RequestFailedException */ - fun invokeFunction(payload: InvokeTransactionPayload): Request + fun invokeFunction(payload: InvokeTransactionV1Payload): Request + + /** + * Invoke a function using version 3 transaction. + * + * Invoke a function in deployed contract using version 3 transaction. + * + * @param payload invoke function version 3 payload + * + * @throws RequestFailedException + */ + fun invokeFunction(payload: InvokeTransactionV3Payload): Request /** * Get the contract class definition. @@ -46,9 +57,9 @@ interface Provider { fun getClass(classHash: Felt): Request /** - * Declare version 1 contract + * Declare contract using version 1 transaction. * - * Declare a version 1 contract on Starknet. + * Declare a contract on Starknet using version 1 transaction. * * @param payload declare transaction version 1 payload * @@ -57,9 +68,9 @@ interface Provider { fun declareContract(payload: DeclareTransactionV1Payload): Request /** - * Declare version 2 contract + * Declare contract using version 2 transaction. * - * Declare a version 2 contract on Starknet. + * Declare a contract on Starknet using version 2 transaction. * * @param payload declare transaction version 2 payload * @@ -67,6 +78,17 @@ interface Provider { */ fun declareContract(payload: DeclareTransactionV2Payload): Request + /** + * Declare contract using version 3 transaction. + * + * Declare a contract on Starknet using version 3 transaction. + * + * @param payload declare transaction version 3 payload + * + * @throws RequestFailedException + */ + fun declareContract(payload: DeclareTransactionV3Payload): Request + /** * Get the block number. * @@ -116,15 +138,26 @@ interface Provider { fun getBlockTransactionCount(blockNumber: Int): Request /** - * Deploy an account contract. + * Deploy an account contract using version 1 transaction. + * + * Deploy a new account contract on Starknet. + * + * @param payload deploy account transaction version 1 payload + * + * @throws RequestFailedException + */ + fun deployAccount(payload: DeployAccountTransactionV1Payload): Request + + /** + * Deploy an account contract using version 3 transaction. * * Deploy a new account contract on Starknet. * - * @param payload deploy account transaction payload + * @param payload deploy account transaction version 3 payload * * @throws RequestFailedException */ - fun deployAccount(payload: DeployAccountTransactionPayload): Request + fun deployAccount(payload: DeployAccountTransactionV3Payload): Request /** * Calls a contract deployed on Starknet. @@ -382,10 +415,27 @@ interface Provider { * @param payload get events payload * @return GetEventsResult with list of emitted events and additional page info * - * @throws RpcRequestFailedException + * @throws RequestFailedException */ fun getEvents(payload: GetEventsPayload): Request + /** + * Estimate a fee. + * + * Estimate a fee for a provided transaction list. + * + * @param payload transaction, for which the fee is to be estimated. + * @param blockHash a hash of the block in respect to what the query will be made + * @param simulationFlags set of flags to be used for simulation. + * + * @throws RequestFailedException + */ + fun getEstimateFee( + payload: List, + blockHash: Felt, + simulationFlags: Set, + ): Request> + /** * Estimate a fee. * @@ -396,7 +446,10 @@ interface Provider { * * @throws RequestFailedException */ - fun getEstimateFee(payload: List, blockHash: Felt): Request> + fun getEstimateFee( + payload: List, + blockHash: Felt, + ): Request> /** * Estimate a fee. @@ -404,11 +457,48 @@ interface Provider { * Estimate a fee for a provided transaction list. * * @param payload transaction, for which the fee is to be estimated. + * @param simulationFlags set of flags to be used for simulation. * @param blockNumber a number of the block in respect to what the query will be made * * @throws RequestFailedException */ - fun getEstimateFee(payload: List, blockNumber: Int): Request> + fun getEstimateFee( + payload: List, + blockNumber: Int, + simulationFlags: Set, + ): Request> + + /** + * Estimate a fee. + * + * Estimate a fee for a provided transaction list. + * + * @param payload transaction, for which the fee is to be estimated. + * @param blockNumber a number of the block in respect to what the query will be made + * + * @throws RequestFailedException + */ + fun getEstimateFee( + payload: List, + blockNumber: Int, + ): Request> + + /** + * Estimate a fee. + * + * Estimate a fee for a provided transaction list. + * + * @param payload transaction, for which the fee is to be estimated. + * @param simulationFlags set of flags to be used for simulation. + * @param blockTag a tag of the block in respect to what the query will be made + * + * @throws RequestFailedException + */ + fun getEstimateFee( + payload: List, + blockTag: BlockTag, + simulationFlags: Set, + ): Request> /** * Estimate a fee. @@ -420,7 +510,10 @@ interface Provider { * * @throws RequestFailedException */ - fun getEstimateFee(payload: List, blockTag: BlockTag): Request> + fun getEstimateFee( + payload: List, + blockTag: BlockTag, + ): Request> /** * Estimate a fee. @@ -428,9 +521,23 @@ interface Provider { * Estimate a fee for a provided transaction list for pending block. * * @param payload transaction, for which the fee is to be estimated. + * @param simulationFlags set of flags to be used for simulation * * @throws RequestFailedException */ + + fun getEstimateFee(payload: List, simulationFlags: Set): Request> + + /** + * Estimate a fee. + * + * Estimate a fee for a provided transaction list for pending block. + * + * @param payload transaction, for which the fee is to be estimated. + * + * @throws RequestFailedException + */ + fun getEstimateFee(payload: List): Request> /** @@ -661,7 +768,8 @@ interface Provider { * * @param transactions list of transactions to be simulated * @param blockTag tag of the block that should be used for simulation - * @param simulationFlags set of flags to be used for simulation * @return a list of transaction simulations + * @param simulationFlags set of flags to be used for simulation + * @return a list of transaction simulations */ fun simulateTransactions(transactions: List, blockTag: BlockTag, simulationFlags: Set): Request> @@ -669,7 +777,8 @@ interface Provider { * * @param transactions list of transactions to be simulated * @param blockNumber number of the block that should be used for simulation - * @param simulationFlags set of flags to be used for simulation * @return a list of transaction simulations + * @param simulationFlags set of flags to be used for simulation + * @return a list of transaction simulations */ fun simulateTransactions(transactions: List, blockNumber: Int, simulationFlags: Set): Request> @@ -677,7 +786,8 @@ interface Provider { * * @param transactions list of transactions to be simulated * @param blockHash hash of the block that should be used for simulation - * @param simulationFlags set of flags to be used for simulation * @return a list of transaction simulations + * @param simulationFlags set of flags to be used for simulation + * @return a list of transaction simulations */ fun simulateTransactions(transactions: List, blockHash: Felt, simulationFlags: Set): Request> } diff --git a/lib/src/main/kotlin/com/swmansion/starknet/provider/rpc/JsonRpcProvider.kt b/lib/src/main/kotlin/com/swmansion/starknet/provider/rpc/JsonRpcProvider.kt index a4c8e8193..95eb473e4 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/provider/rpc/JsonRpcProvider.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/provider/rpc/JsonRpcProvider.kt @@ -34,6 +34,10 @@ class JsonRpcProvider( private val jsonWithDefaults = Json { encodeDefaults = true } + private val defaultFeeEstimateSimulationFlags: Set by lazy { + setOf(SimulationFlagForEstimateFee.SKIP_VALIDATE) + } + private fun buildRequestJson(method: String, paramsJson: JsonElement): Map { val map = mapOf( "jsonrpc" to JsonPrimitive("2.0"), @@ -92,7 +96,16 @@ class JsonRpcProvider( return callContract(call, BlockTag.LATEST) } - override fun deployAccount(payload: DeployAccountTransactionPayload): Request { + override fun deployAccount(payload: DeployAccountTransactionV1Payload): Request { + val params = jsonWithDefaults.encodeToJsonElement(payload) + val jsonPayload = buildJsonObject { + put("deploy_account_transaction", params) + } + + return buildRequest(JsonRpcMethod.DEPLOY_ACCOUNT_TRANSACTION, jsonPayload, DeployAccountResponse.serializer()) + } + + override fun deployAccount(payload: DeployAccountTransactionV3Payload): Request { val params = jsonWithDefaults.encodeToJsonElement(payload) val jsonPayload = buildJsonObject { put("deploy_account_transaction", params) @@ -155,7 +168,18 @@ class JsonRpcProvider( } override fun invokeFunction( - payload: InvokeTransactionPayload, + payload: InvokeTransactionV1Payload, + ): Request { + val params = jsonWithDefaults.encodeToJsonElement(payload) + val jsonPayload = buildJsonObject { + put("invoke_transaction", params) + } + + return buildRequest(JsonRpcMethod.INVOKE_TRANSACTION, jsonPayload, InvokeFunctionResponse.serializer()) + } + + override fun invokeFunction( + payload: InvokeTransactionV3Payload, ): Request { val params = jsonWithDefaults.encodeToJsonElement(payload) val jsonPayload = buildJsonObject { @@ -267,6 +291,15 @@ class JsonRpcProvider( return buildRequest(JsonRpcMethod.DECLARE, jsonPayload, DeclareResponse.serializer()) } + override fun declareContract(payload: DeclareTransactionV3Payload): Request { + val params = jsonWithDefaults.encodeToJsonElement(DeclareTransactionV3PayloadSerializer, payload) + val jsonPayload = buildJsonObject { + put("declare_transaction", params) + } + + return buildRequest(JsonRpcMethod.DECLARE, jsonPayload, DeclareResponse.serializer()) + } + override fun getBlockNumber(): Request { val params = Json.encodeToJsonElement(JsonArray(emptyList())) @@ -325,31 +358,67 @@ class JsonRpcProvider( } private fun getEstimateFee(payload: EstimateTransactionFeePayload): Request> { - val jsonPayload = jsonWithDefaults.encodeToJsonElement(EstimateTransactionFeePayloadSerializer, payload) + val jsonPayload = jsonWithDefaults.encodeToJsonElement(payload) return buildRequest(JsonRpcMethod.ESTIMATE_FEE, jsonPayload, ListSerializer(EstimateFeeResponse.serializer())) } - override fun getEstimateFee(payload: List, blockHash: Felt): Request> { - val estimatePayload = EstimateTransactionFeePayload(payload, BlockId.Hash(blockHash)) + override fun getEstimateFee( + payload: List, + blockHash: Felt, + simulationFlags: Set, + ): Request> { + val estimatePayload = EstimateTransactionFeePayload(payload, simulationFlags, BlockId.Hash(blockHash)) return getEstimateFee(estimatePayload) } - override fun getEstimateFee(payload: List, blockNumber: Int): Request> { - val estimatePayload = EstimateTransactionFeePayload(payload, BlockId.Number(blockNumber)) + override fun getEstimateFee( + payload: List, + blockHash: Felt, + ): Request> { + return getEstimateFee(payload, blockHash, defaultFeeEstimateSimulationFlags) + } + + override fun getEstimateFee( + payload: List, + blockNumber: Int, + simulationFlags: Set, + ): Request> { + val estimatePayload = EstimateTransactionFeePayload(payload, simulationFlags, BlockId.Number(blockNumber)) return getEstimateFee(estimatePayload) } - override fun getEstimateFee(payload: List, blockTag: BlockTag): Request> { - val estimatePayload = EstimateTransactionFeePayload(payload, BlockId.Tag(blockTag)) + override fun getEstimateFee( + payload: List, + blockNumber: Int, + ): Request> { + return getEstimateFee(payload, blockNumber, defaultFeeEstimateSimulationFlags) + } + + override fun getEstimateFee( + payload: List, + blockTag: BlockTag, + simulationFlags: Set, + ): Request> { + val estimatePayload = EstimateTransactionFeePayload(payload, simulationFlags, BlockId.Tag(blockTag)) return getEstimateFee(estimatePayload) } + override fun getEstimateFee( + payload: List, + blockTag: BlockTag, + ): Request> { + return getEstimateFee(payload, blockTag, defaultFeeEstimateSimulationFlags) + } + + override fun getEstimateFee(payload: List, simulationFlags: Set): Request> { + return getEstimateFee(payload, BlockTag.PENDING, simulationFlags) + } override fun getEstimateFee(payload: List): Request> { - return getEstimateFee(payload, BlockTag.LATEST) + return getEstimateFee(payload, BlockTag.PENDING, defaultFeeEstimateSimulationFlags) } private fun getEstimateMessageFee(payload: EstimateMessageFeePayload): Request { diff --git a/lib/src/main/kotlin/com/swmansion/starknet/signer/StarkCurveSigner.kt b/lib/src/main/kotlin/com/swmansion/starknet/signer/StarkCurveSigner.kt index 4c9f74816..a6c9a0ef4 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/signer/StarkCurveSigner.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/signer/StarkCurveSigner.kt @@ -16,9 +16,8 @@ class StarkCurveSigner(private val privateKey: Felt) : Signer { override val publicKey: Felt by lazy { StarknetCurve.getPublicKey(privateKey) } override fun signTransaction(transaction: Transaction): Signature { - if (transaction.hash == null) { - throw IllegalArgumentException("Invalid transaction: hash is missing.") - } + // TODO: if hash is missing, generate it on demand + requireNotNull(transaction.hash) { "Invalid transaction: hash is missing." } return StarknetCurve.sign(privateKey, transaction.hash!!).toList() } diff --git a/lib/src/test/kotlin/com/swmansion/starknet/data/TransactionHashCalculatorTest.kt b/lib/src/test/kotlin/com/swmansion/starknet/data/TransactionHashCalculatorTest.kt index 53690b1cd..bae81532e 100644 --- a/lib/src/test/kotlin/com/swmansion/starknet/data/TransactionHashCalculatorTest.kt +++ b/lib/src/test/kotlin/com/swmansion/starknet/data/TransactionHashCalculatorTest.kt @@ -13,7 +13,7 @@ internal class TransactionHashCalculatorTest { @Test fun calculateInvokeTxHash() { - val hash = TransactionHashCalculator.calculateInvokeTxHash( + val hash = TransactionHashCalculator.calculateInvokeTxV1Hash( contractAddress = Felt.fromHex("0x6352037a8acbb31095a8ed0f4aa8d8639e13b705b043a1b08f9640d2f9f0d56"), calldata = calldata, chainId = chainId, @@ -28,7 +28,7 @@ internal class TransactionHashCalculatorTest { @Test fun calculateDeployAccountTxHash() { - val hash = TransactionHashCalculator.calculateDeployAccountTxHash( + val hash = TransactionHashCalculator.calculateDeployAccountV1TxHash( classHash = Felt.fromHex("0x21a7f43387573b68666669a0ed764252ce5367708e696e31967764a90b429c2"), calldata = calldata, salt = Felt(1234), diff --git a/lib/src/test/kotlin/network/account/AccountTest.kt b/lib/src/test/kotlin/network/account/AccountTest.kt index 038e91028..02498f227 100644 --- a/lib/src/test/kotlin/network/account/AccountTest.kt +++ b/lib/src/test/kotlin/network/account/AccountTest.kt @@ -7,6 +7,7 @@ import com.swmansion.starknet.data.types.* import com.swmansion.starknet.data.types.transactions.* import com.swmansion.starknet.deployercontract.StandardDeployer import com.swmansion.starknet.extensions.toFelt +import com.swmansion.starknet.extensions.toUint256 import com.swmansion.starknet.provider.rpc.JsonRpcProvider import com.swmansion.starknet.signer.StarkCurveSigner import network.utils.NetworkConfig @@ -32,7 +33,6 @@ class AccountTest { private val signer = StarkCurveSigner(config.privateKey) private val constNonceAccountAddress = config.constNonceAccountAddress ?: config.accountAddress private val constNonceSigner = StarkCurveSigner(config.constNoncePrivateKey ?: config.privateKey) - private val provider = JsonRpcProvider(rpcUrl, StarknetChainId.TESTNET) val standardAccount = StandardAccount( @@ -56,6 +56,7 @@ class AccountTest { Network.TESTNET -> Felt.fromHex("0x02BAe9749940E7b89613C1a21D9C832242447caA065D5A2b8AB08c0c469b3462") } private val ethContractAddress = Felt.fromHex("0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7") // Same for testnet and integration. + private val strkContractAddress = Felt.fromHex("0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d") private val udcAddress = Felt.fromHex("0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf") // Same for testnet and integration. } @@ -111,7 +112,7 @@ class AccountTest { version = declareTransactionPayload.version, ) - val feeEstimateRequest = provider.getEstimateFee(listOf(signedTransaction.toPayload()), BlockTag.LATEST) + val feeEstimateRequest = provider.getEstimateFee(listOf(signedTransaction.toPayload()), BlockTag.LATEST, emptySet()) val feeEstimate = feeEstimateRequest.send().first() assertNotEquals(Felt(0), feeEstimate.gasConsumed) @@ -157,7 +158,44 @@ class AccountTest { version = declareTransactionPayload.version, ) - val feeEstimateRequest = provider.getEstimateFee(listOf(signedTransaction.toPayload()), BlockTag.LATEST) + val feeEstimateRequest = provider.getEstimateFee(listOf(signedTransaction.toPayload()), BlockTag.LATEST, emptySet()) + + val feeEstimate = feeEstimateRequest.send().first() + assertNotEquals(Felt(0), feeEstimate.gasConsumed) + assertNotEquals(Felt(0), feeEstimate.gasPrice) + assertNotEquals(Felt(0), feeEstimate.overallFee) + } + + @Test + fun `estimate fee for declare v3 transaction`() { + assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) + + val account = constNonceAccount + + ScarbClient.createSaltedContract( + placeholderContractPath = Path.of("src/test/resources/contracts_v2/src/placeholder_counter_contract.cairo"), + saltedContractPath = Path.of("src/test/resources/contracts_v2/src/salted_counter_contract.cairo"), + ) + ScarbClient.buildContracts(Path.of("src/test/resources/contracts_v2")) + val contractCode = Path.of("src/test/resources/contracts_v2/target/release/ContractsV2_SaltedCounterContract.sierra.json").readText() + val casmCode = Path.of("src/test/resources/contracts_v2/target/release/ContractsV2_SaltedCounterContract.casm.json").readText() + + val contractDefinition = Cairo1ContractDefinition(contractCode) + val casmContractDefinition = CasmContractDefinition(casmCode) + + val nonce = account.getNonce().send() + val params = DeclareParamsV3( + nonce = nonce, + l1ResourceBounds = ResourceBounds.ZERO, + ) + val declareTransactionPayload = account.signDeclare( + sierraContractDefinition = contractDefinition, + casmContractDefinition = casmContractDefinition, + params = params, + forFeeEstimate = true, + ) + + val feeEstimateRequest = provider.getEstimateFee(listOf(declareTransactionPayload), BlockTag.PENDING) val feeEstimate = feeEstimateRequest.send().first() assertNotEquals(Felt(0), feeEstimate.gasConsumed) @@ -271,7 +309,49 @@ class AccountTest { } @Test - fun `sign and send invoke transaction`() { + fun `sign and send declare v3 transaction`() { + assumeTrue(NetworkConfig.isTestEnabled(requiresGas = true)) + // Note to future developers experiencing experiencing failures in this test. + // This test sometimes fails due to getNonce receiving higher (pending) nonce than addDeclareTransaction expects + + val account = standardAccount + + ScarbClient.createSaltedContract( + placeholderContractPath = Path.of("src/test/resources/contracts_v2/src/placeholder_counter_contract.cairo"), + saltedContractPath = Path.of("src/test/resources/contracts_v2/src/salted_counter_contract.cairo"), + ) + ScarbClient.buildContracts(Path.of("src/test/resources/contracts_v2")) + val contractCode = Path.of("src/test/resources/contracts_v2/target/release/ContractsV2_SaltedCounterContract.sierra.json").readText() + val casmCode = Path.of("src/test/resources/contracts_v2/target/release/ContractsV2_SaltedCounterContract.casm.json").readText() + + val contractDefinition = Cairo1ContractDefinition(contractCode) + val contractCasmDefinition = CasmContractDefinition(casmCode) + val nonce = account.getNonce().send() + + val l1ResourceBounds = ResourceBounds( + maxAmount = Uint64(5000), + maxPricePerUnit = Uint128(200000000), + ) + val params = DeclareParamsV3( + nonce = nonce, + l1ResourceBounds = l1ResourceBounds, + ) + val declareTransactionPayload = account.signDeclare( + contractDefinition, + contractCasmDefinition, + params, + ) + val request = provider.declareContract(declareTransactionPayload) + val result = request.send() + + Thread.sleep(60000) + val receipt = provider.getTransactionReceipt(result.transactionHash).send() + + assertTrue(receipt.isAccepted) + } + + @Test + fun `sign and send invoke v1 transaction`() { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = true)) // Note to future developers experiencing experiencing failures in this test. // This test sometimes fails due to getNonce receiving higher (pending) nonce than addInvokeTransaction expects @@ -293,6 +373,29 @@ class AccountTest { assertTrue(receipt.isAccepted) } + @Test + fun `sign and send invoke v3 transaction`() { + assumeTrue(NetworkConfig.isTestEnabled(requiresGas = true)) + // Note to future developers experiencing experiencing failures in this test. + // This test sometimes fails due to getNonce receiving higher (pending) nonce than addInvokeTransaction expects + + val account = standardAccount + + val call = Call( + contractAddress = predeployedMapContractAddress, + entrypoint = "put", + calldata = listOf(Felt.fromHex("0x1D2C3B7A8"), Felt.fromHex("0x451")), + ) + + val invokeRequest = account.executeV3(call) + val invokeResponse = invokeRequest.send() + + Thread.sleep(10000) + + val receipt = provider.getTransactionReceipt(invokeResponse.transactionHash).send() + assertTrue(receipt.isAccepted) + } + @Test fun `call contract`() { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) @@ -394,7 +497,7 @@ class AccountTest { } @Test - fun `deploy account`() { + fun `sign and send deploy account v1 transaction`() { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = true)) val account = standardAccount @@ -452,7 +555,7 @@ class AccountTest { // Make sure tx matches what we sent Thread.sleep(15000) - val tx = provider.getTransaction(response.transactionHash).send() as DeployAccountTransaction + val tx = provider.getTransaction(response.transactionHash).send() as DeployAccountTransactionV1 assertEquals(payload.classHash, tx.classHash) assertEquals(payload.salt, tx.contractAddressSalt) assertEquals(payload.constructorCalldata, tx.constructorCalldata) @@ -465,6 +568,91 @@ class AccountTest { assertTrue(receipt.isAccepted) } + @Test + fun `sign and send deploy account v3 transaction`() { + assumeTrue(NetworkConfig.isTestEnabled(requiresGas = true)) + + val account = standardAccount + + val privateKey = Felt(System.currentTimeMillis()) + val publicKey = StarknetCurve.getPublicKey(privateKey) + + val classHash = accountContractClassHash + val salt = Felt(System.currentTimeMillis()) + val calldata = listOf(publicKey) + val deployedAccountAddress = ContractAddressCalculator.calculateAddressFromHash( + classHash = classHash, + calldata = calldata, + salt = salt, + ) + + val deployedAccount = StandardAccount( + deployedAccountAddress, + privateKey, + provider, + ) + val payloadForFeeEstimate = deployedAccount.signDeployAccount( + classHash = classHash, + salt = salt, + calldata = calldata, + params = DeployAccountParamsV3( + nonce = Felt.ZERO, + l1ResourceBounds = ResourceBounds.ZERO, + ), + forFeeEstimate = false, // BUG: (#344) this should be true, but Pathfinder and Devnet claim that using query version produce invalid signature + ) + val feeEstimateRequest = provider.getEstimateFee(listOf(payloadForFeeEstimate)) + + val feeEstimate = feeEstimateRequest.send().first() + val resourceBounds = feeEstimate.toResourceBounds() + + val call = Call( + contractAddress = strkContractAddress, + entrypoint = "transfer", + calldata = listOf(deployedAccountAddress) + + resourceBounds.l1Gas.toMaxFee().toUint256.toCalldata(), + ) + + val transferRequest = account.executeV3(call) + val transferResponse = transferRequest.send() + Thread.sleep(15000) + + val transferReceipt = provider.getTransactionReceipt(transferResponse.transactionHash).send() + assertTrue(transferReceipt.isAccepted) + + val params = DeployAccountParamsV3( + nonce = Felt.ZERO, + resourceBounds = resourceBounds, + ) + val payload = deployedAccount.signDeployAccount( + classHash = classHash, + salt = salt, + calldata = calldata, + params = params, + forFeeEstimate = false, + ) + + val response = provider.deployAccount(payload).send() + + // Make sure the address matches the calculated one + // TODO: (#344) re-enable this assertion once the address is non-nullable again + // assertEquals(deployedAccountAddress, response.address) + + // Make sure tx matches what we sent + Thread.sleep(15000) + + val tx = provider.getTransaction(response.transactionHash).send() as DeployAccountTransactionV3 + assertEquals(payload.classHash, tx.classHash) + assertEquals(payload.salt, tx.contractAddressSalt) + assertEquals(payload.constructorCalldata, tx.constructorCalldata) + assertEquals(payload.version, tx.version) + assertEquals(payload.nonce, tx.nonce) + assertEquals(payload.signature, tx.signature) + + val receipt = provider.getTransactionReceipt(response.transactionHash).send() + assertTrue(receipt.isAccepted) + } + @Test fun `simulate multiple invoke transactions`() { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) @@ -521,8 +709,8 @@ class AccountTest { // Juno currently does not support SKIP_VALIDATE flag if (network != Network.TESTNET) { - val invokeTxWithoutSignature = InvokeTransactionPayload(invokeTx.senderAddress, invokeTx.calldata, emptyList(), invokeTx.maxFee, invokeTx.version, invokeTx.nonce) - val invokeTxWihtoutSignature2 = InvokeTransactionPayload(invokeTx2.senderAddress, invokeTx2.calldata, emptyList(), invokeTx2.maxFee, invokeTx2.version, invokeTx2.nonce) + val invokeTxWithoutSignature = InvokeTransactionV1Payload(invokeTx.senderAddress, invokeTx.calldata, emptyList(), invokeTx.maxFee, invokeTx.version, invokeTx.nonce) + val invokeTxWihtoutSignature2 = InvokeTransactionV1Payload(invokeTx2.senderAddress, invokeTx2.calldata, emptyList(), invokeTx2.maxFee, invokeTx2.version, invokeTx2.nonce) val simulationFlags2 = setOf(SimulationFlag.SKIP_FEE_CHARGE, SimulationFlag.SKIP_VALIDATE) val simulationResult2 = provider.simulateTransactions( transactions = listOf(invokeTxWithoutSignature, invokeTxWihtoutSignature2), @@ -573,7 +761,7 @@ class AccountTest { // Juno currently does not support SKIP_VALIDATE flag if (network != Network.TESTNET) { - val deployAccountTxWithoutSignature = DeployAccountTransactionPayload(deployAccountTx.classHash, deployAccountTx.salt, deployAccountTx.constructorCalldata, deployAccountTx.version, deployAccountTx.nonce, deployAccountTx.maxFee, emptyList()) + val deployAccountTxWithoutSignature = DeployAccountTransactionV1Payload(deployAccountTx.classHash, deployAccountTx.salt, deployAccountTx.constructorCalldata, deployAccountTx.version, deployAccountTx.nonce, deployAccountTx.maxFee, emptyList()) val simulationFlags2 = setOf(SimulationFlag.SKIP_FEE_CHARGE, SimulationFlag.SKIP_VALIDATE) val simulationResult2 = provider.simulateTransactions( diff --git a/lib/src/test/kotlin/network/provider/ProviderTest.kt b/lib/src/test/kotlin/network/provider/ProviderTest.kt index 11db98286..893afeb0e 100644 --- a/lib/src/test/kotlin/network/provider/ProviderTest.kt +++ b/lib/src/test/kotlin/network/provider/ProviderTest.kt @@ -47,12 +47,18 @@ class ProviderTest { Network.TESTNET -> Felt.fromHex("0x6bf08a6547a8be3cd3d718a068c2c0e9d3820252935f766c1ba6dd46f62e05") } val transactionStatus = provider.getTransactionStatus(transactionHash).send() - assertEquals(TransactionStatus.ACCEPTED_ON_L1, transactionStatus.finalityStatus) + // TODO: Re-enable this assertion for integration once transaction appear as accepted on L1 again + if (network != Network.INTEGRATION) { + assertEquals(TransactionFinalityStatus.ACCEPTED_ON_L1, transactionStatus.finalityStatus) + } assertNotNull(transactionStatus.executionStatus) assertEquals(TransactionExecutionStatus.SUCCEEDED, transactionStatus.executionStatus) val transactionStatus2 = provider.getTransactionStatus(transactionHash2).send() - assertEquals(TransactionStatus.ACCEPTED_ON_L1, transactionStatus2.finalityStatus) + // TODO: Re-enable this assertion for integration once transaction appear as accepted on L1 again + if (network != Network.INTEGRATION) { + assertEquals(TransactionFinalityStatus.ACCEPTED_ON_L1, transactionStatus2.finalityStatus) + } assertNotNull(transactionStatus2.executionStatus) assertEquals(TransactionExecutionStatus.REVERTED, transactionStatus2.executionStatus) } @@ -109,7 +115,7 @@ class ProviderTest { } @Test - fun `get deploy account transaction`() { + fun `get deploy account v1 transaction`() { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) val transactionHash = when (network) { @@ -118,6 +124,7 @@ class ProviderTest { } val tx = provider.getTransaction(transactionHash).send() assertEquals(transactionHash, tx.hash) + assertEquals(Felt.ONE, tx.version) val receiptRequest = provider.getTransactionReceipt(transactionHash) val receipt = receiptRequest.send() @@ -128,7 +135,29 @@ class ProviderTest { } @Test - fun `get reverted invoke transaction`() { + fun `get deploy account v3 transaction`() { + assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) + + assumeTrue(network == Network.INTEGRATION) + val transactionHash = when (network) { + Network.INTEGRATION -> Felt.fromHex("0x007c1ca558aaec1a14a4c0553517013631fad81c48667a3bcd635617c2560276") + Network.TESTNET -> throw NotImplementedError("No support for testing deploy account v3 on testnet") + } + + val tx = provider.getTransaction(transactionHash).send() + assertEquals(transactionHash, tx.hash) + assertEquals(Felt(3), tx.version) + + val receiptRequest = provider.getTransactionReceipt(transactionHash) + val receipt = receiptRequest.send() + + assertTrue(receipt is ProcessedDeployAccountTransactionReceipt) + assertTrue(receipt.isAccepted) + assertNull(receipt.revertReason) + } + + @Test + fun `get reverted invoke v1 transaction`() { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) val transactionHash = when (network) { @@ -144,16 +173,19 @@ class ProviderTest { assertTrue(receipt is ProcessedInvokeTransactionReceipt) assertFalse(receipt.isAccepted) assertEquals(TransactionExecutionStatus.REVERTED, receipt.executionStatus) - assertEquals(TransactionFinalityStatus.ACCEPTED_ON_L1, receipt.finalityStatus) + // TODO: Re-enable this assertion for integration once transaction appear as accepted on L1 again + if (network != Network.INTEGRATION) { + assertEquals(TransactionFinalityStatus.ACCEPTED_ON_L1, receipt.finalityStatus) + } assertNotNull(receipt.revertReason) } @Test - fun `get invoke transaction with events`() { + fun `get invoke v1 transaction with events`() { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) val transactionHash = when (network) { - Network.INTEGRATION -> Felt.fromHex("0x26396c032286bcefb54616581eea5c7e373f0a21c322c44912cfa0944a52926") + Network.INTEGRATION -> Felt.fromHex("0x34223514e92989608e3b36f2a2a53011fa0699a275d7936a18921a11963c792") Network.TESTNET -> Felt.fromHex("0x72776cb6462e7e1268bd93dee8ad2df5ee0abed955e3010182161bdb0daea62") } val tx = provider.getTransaction(transactionHash).send() @@ -173,6 +205,34 @@ class ProviderTest { assertTrue(receipt is ProcessedInvokeTransactionReceipt) } + @Test + fun `get invoke v3 transaction with events`() { + assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) + + assumeTrue(network == Network.INTEGRATION) + val transactionHash = when (network) { + Network.INTEGRATION -> Felt.fromHex("0x06f99b0650eb02eaf16cc97820075b1dc8c8a4ada22ef0a606f3c0b066d7ce07") + Network.TESTNET -> throw NotImplementedError("No support for testing deploy account v3 on testnet") + } + + val tx = provider.getTransaction(transactionHash).send() + assertTrue(tx is InvokeTransaction) + assertEquals(transactionHash, tx.hash) + assertEquals(TransactionType.INVOKE, tx.type) + assertEquals(Felt(3), tx.version) + + val receiptRequest = provider.getTransactionReceipt(transactionHash) + val receipt = receiptRequest.send() + + assertTrue(receipt.isAccepted) + assertTrue(receipt.events.size >= 2) + + assertTrue(receipt.isAccepted) + assertNull(receipt.revertReason) + + assertTrue(receipt is ProcessedInvokeTransactionReceipt) + } + @Test fun `get declare v0 transaction`() { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) @@ -187,11 +247,15 @@ class ProviderTest { val receipt = receiptRequest.send() assertTrue(receipt.isAccepted) - assertEquals(TransactionFinalityStatus.ACCEPTED_ON_L1, receipt.finalityStatus) + // TODO: Re-enable this assertion for integration once transaction appear as accepted on L1 again + if (network != Network.INTEGRATION) { + assertEquals(TransactionFinalityStatus.ACCEPTED_ON_L1, receipt.finalityStatus) + } assertNull(receipt.revertReason) receipt is ProcessedDeclareTransactionReceipt - assertEquals(Felt.ZERO, receipt.actualFee) + assertEquals(Felt.ZERO, receipt.actualFee.amount) + assertEquals(PriceUnit.WEI, receipt.actualFee.unit) } @Test @@ -199,7 +263,7 @@ class ProviderTest { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) val transactionHash = when (network) { - Network.INTEGRATION -> Felt.fromHex("0x6d346ba207eb124355960c19c737698ad37a3c920a588b741e0130ff5bd4d6d") + Network.INTEGRATION -> Felt.fromHex("0x0417ec8ece9d2d2e68307069fdcde3c1fd8b0713b8a2687b56c19455c6ea85c1") Network.TESTNET -> Felt.fromHex("0x6801a86a4a6873f62aaa478151ba03171691edde897c434ec8cf9db3bb77573") } val tx = provider.getTransaction(transactionHash).send() as DeclareTransactionV1 @@ -212,7 +276,10 @@ class ProviderTest { assertTrue(receipt is ProcessedDeclareTransactionReceipt) assertTrue(receipt.isAccepted) assertEquals(TransactionExecutionStatus.SUCCEEDED, receipt.executionStatus) - assertEquals(TransactionFinalityStatus.ACCEPTED_ON_L1, receipt.finalityStatus) + // TODO: Re-enable this assertion for integration once transaction appear as accepted on L1 again + if (network != Network.INTEGRATION) { + assertEquals(TransactionFinalityStatus.ACCEPTED_ON_L1, receipt.finalityStatus) + } assertNull(receipt.revertReason) } @@ -236,6 +303,28 @@ class ProviderTest { assertNull(receipt.revertReason) } + @Test + fun `get declare v3 transaction`() { + assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) + + assumeTrue(network == Network.INTEGRATION) + val transactionHash = when (network) { + Network.INTEGRATION -> Felt.fromHex("0x86693a36721bb586bee1f8c8b9ea33fbbb7f820dde48d9068dfa94a99ef53") + Network.TESTNET -> throw NotImplementedError("No support for testing deploy account v3 on testnet") + } + + val tx = provider.getTransaction(transactionHash).send() as DeclareTransactionV3 + assertNotEquals(Felt.ZERO, tx.classHash) + assertEquals(transactionHash, tx.hash) + + val receiptRequest = provider.getTransactionReceipt(transactionHash) + val receipt = receiptRequest.send() + + assertTrue(receipt is ProcessedDeclareTransactionReceipt) + assertTrue(receipt.isAccepted) + assertNull(receipt.revertReason) + } + @Test fun `get l1 handler transaction`() { assumeTrue(NetworkConfig.isTestEnabled(requiresGas = false)) diff --git a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt index 089efc294..7eea1eca5 100644 --- a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt +++ b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt @@ -7,6 +7,7 @@ import com.swmansion.starknet.data.ContractAddressCalculator import com.swmansion.starknet.data.selectorFromName import com.swmansion.starknet.data.types.* import com.swmansion.starknet.data.types.transactions.* +import com.swmansion.starknet.extensions.toUint256 import com.swmansion.starknet.provider.exceptions.RequestFailedException import com.swmansion.starknet.provider.rpc.JsonRpcProvider import com.swmansion.starknet.service.http.HttpResponse @@ -16,6 +17,7 @@ import com.swmansion.starknet.signer.StarkCurveSigner import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.ExecutionMode @@ -55,8 +57,10 @@ class StandardAccountTest { try { devnetClient.start() - // Prepare devnet address book - val accountDetails = devnetClient.createDeployAccount("standard_account_test").details + // TODO: (#371) instead, deploy manutally "standard_account_test" account + // and prefund it with STRK, once minting STRK is supported on devnet. + val accountDetails = DevnetClient.predeployedAccount1 + devnetClient.prefundAccountEth(accountDetails.address) balanceContractAddress = devnetClient.declareDeployContract("Balance", constructorCalldata = listOf(Felt(451))).contractAddress accountAddress = accountDetails.address @@ -86,85 +90,113 @@ class StandardAccountTest { StandardAccount(Felt.ZERO, privateKey, provider) } - @Test - fun `get nonce`() { - val nonce = account.getNonce().send() - assert(nonce >= Felt.ZERO) - } + @Nested + inner class NonceTest { + @Test + fun `get nonce`() { + val nonce = account.getNonce().send() + assert(nonce >= Felt.ZERO) + } - @Test - fun `get nonce at latest block tag`() { - val nonce = account.getNonce(BlockTag.LATEST).send() - assert(nonce >= Felt.ZERO) - } + @Test + fun `get nonce at latest block tag`() { + val nonce = account.getNonce(BlockTag.LATEST).send() + assert(nonce >= Felt.ZERO) + } - @Test - fun `get nonce at block hash`() { - val blockHashAndNumber = provider.getBlockHashAndNumber().send() + @Test + fun `get nonce at block hash`() { + val blockHashAndNumber = provider.getBlockHashAndNumber().send() - val nonce = account.getNonce(blockHashAndNumber.blockHash).send() - assert(nonce >= Felt.ZERO) - } + val nonce = account.getNonce(blockHashAndNumber.blockHash).send() + assert(nonce >= Felt.ZERO) + } - @Test - fun `get nonce at block number`() { - val blockNumber = provider.getBlockNumber().send() + @Test + fun `get nonce at block number`() { + val blockNumber = provider.getBlockNumber().send() - val nonce = account.getNonce(blockNumber).send() - assert(nonce >= Felt.ZERO) - } + val nonce = account.getNonce(blockNumber).send() + assert(nonce >= Felt.ZERO) + } - @Test - fun `get nonce twice`() { - val startNonce = account.getNonce().send() - val call = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10)), - ) - account.execute(call).send() + @Test + fun `get nonce twice`() { + val startNonce = account.getNonce().send() + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + account.execute(call).send() - val endNonce = account.getNonce().send() - assertEquals( - startNonce.value + Felt.ONE.value, - endNonce.value, - ) + val endNonce = account.getNonce().send() + assertEquals( + startNonce.value + Felt.ONE.value, + endNonce.value, + ) + } } - @Test - fun `estimate fee for invoke transaction`() { - val call = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10)), - ) + @Nested + inner class InvokeEstimateTest { + @Test + fun `estimate fee for invoke v1 transaction`() { + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) - val request = account.estimateFee(call) - val response = request.send() - val feeEstimate = response.first() + val request = account.estimateFee(call) + val response = request.send() + val feeEstimate = response.first() - assertNotEquals(Felt.ZERO, feeEstimate.gasPrice) - assertNotEquals(Felt.ZERO, feeEstimate.gasConsumed) - assertNotEquals(Felt.ZERO, feeEstimate.overallFee) - assertEquals(feeEstimate.gasPrice.value.multiply(feeEstimate.gasConsumed.value), feeEstimate.overallFee.value) - } + assertNotEquals(Felt.ZERO, feeEstimate.gasPrice) + assertNotEquals(Felt.ZERO, feeEstimate.gasConsumed) + assertNotEquals(Felt.ZERO, feeEstimate.overallFee) + assertEquals(feeEstimate.gasPrice.value.multiply(feeEstimate.gasConsumed.value), feeEstimate.overallFee.value) + } - @Test - fun `estimate fee for invoke transaction at latest block tag`() { - val call = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10)), - ) + @Test + fun `estimate fee for invoke v3 transaction`() { + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) - val request = account.estimateFee(call, BlockTag.LATEST) - val response = request.send() - val feeEstimate = response.first() + val simulationFlags = emptySet() + val request = account.estimateFeeV3( + listOf(call), + simulationFlags, + ) + val response = request.send() + val feeEstimate = response.first() + + assertNotEquals(Felt.ZERO, feeEstimate.gasPrice) + assertNotEquals(Felt.ZERO, feeEstimate.gasConsumed) + assertNotEquals(Felt.ZERO, feeEstimate.overallFee) + assertEquals(feeEstimate.gasPrice.value.multiply(feeEstimate.gasConsumed.value), feeEstimate.overallFee.value) + } - assertNotEquals(Felt.ZERO, feeEstimate.gasPrice) - assertNotEquals(Felt.ZERO, feeEstimate.gasConsumed) - assertNotEquals(Felt.ZERO, feeEstimate.overallFee) - assertEquals(feeEstimate.gasPrice.value.multiply(feeEstimate.gasConsumed.value), feeEstimate.overallFee.value) + @Test + fun `estimate fee for invoke transaction at latest block tag`() { + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + + val request = account.estimateFee(call, BlockTag.LATEST) + val response = request.send() + val feeEstimate = response.first() + + assertNotEquals(Felt.ZERO, feeEstimate.gasPrice) + assertNotEquals(Felt.ZERO, feeEstimate.gasConsumed) + assertNotEquals(Felt.ZERO, feeEstimate.overallFee) + assertEquals(feeEstimate.gasPrice.value.multiply(feeEstimate.gasConsumed.value), feeEstimate.overallFee.value) + } } @Test @@ -194,14 +226,37 @@ class StandardAccountTest { version = declareTransactionPayload.version, ) - val request = provider.getEstimateFee(listOf(signedTransaction.toPayload()), BlockTag.LATEST) + val request = provider.getEstimateFee( + listOf(signedTransaction.toPayload()), + BlockTag.LATEST, + emptySet(), + ) val response = request.send() val feeEstimate = response.first() - assertNotEquals(Felt.ZERO, feeEstimate.gasPrice) - assertNotEquals(Felt.ZERO, feeEstimate.gasConsumed) - assertNotEquals(Felt.ZERO, feeEstimate.overallFee) - assertEquals(feeEstimate.gasPrice.value.multiply(feeEstimate.gasConsumed.value), feeEstimate.overallFee.value) + val txWithoutSignature = signedTransaction.copy(signature = emptyList()) + val request2 = provider.getEstimateFee( + listOf(txWithoutSignature.toPayload()), + BlockTag.LATEST, + setOf(SimulationFlagForEstimateFee.SKIP_VALIDATE), + ) + val response2 = request2.send() + val feeEstimate2 = response2.first() + + listOf(feeEstimate, feeEstimate2).forEach { + assertNotEquals(Felt.ZERO, it.gasPrice) + assertNotEquals(Felt.ZERO, it.gasConsumed) + assertNotEquals(Felt.ZERO, it.overallFee) + assertEquals(it.gasPrice.value.multiply(it.gasConsumed.value), it.overallFee.value) + } + + assertThrows(RequestFailedException::class.java) { + provider.getEstimateFee( + listOf(txWithoutSignature.toPayload()), + BlockTag.LATEST, + emptySet(), + ).send() + } } // TODO: Use message mocking instead of deploying l1l2 contract. @@ -318,23 +373,36 @@ class StandardAccountTest { } @Test - fun `sign single call test`() { - val call = Call( - contractAddress = balanceContractAddress, - calldata = listOf(Felt(10)), - entrypoint = "increase_balance", + fun `sign and send declare v3 transaction`() { + ScarbClient.createSaltedContract( + placeholderContractPath = Path.of("src/test/resources/contracts_v2/src/placeholder_counter_contract.cairo"), + saltedContractPath = Path.of("src/test/resources/contracts_v2/src/salted_counter_contract.cairo"), ) + ScarbClient.buildContracts(Path.of("src/test/resources/contracts_v2")) + val contractCode = Path.of("src/test/resources/contracts_v2/target/release/ContractsV2_SaltedCounterContract.sierra.json").readText() + val casmCode = Path.of("src/test/resources/contracts_v2/target/release/ContractsV2_SaltedCounterContract.casm.json").readText() - val params = ExecutionParams( - maxFee = Felt(1000000000000000), - nonce = account.getNonce().send(), - ) + val contractDefinition = Cairo2ContractDefinition(contractCode) + val contractCasmDefinition = CasmContractDefinition(casmCode) + val nonce = account.getNonce().send() - val payload = account.sign(call, params) - val request = provider.invokeFunction(payload) - val response = request.send() + val params = DeclareParamsV3( + nonce = nonce, + l1ResourceBounds = ResourceBounds( + maxAmount = Uint64(20000), + maxPricePerUnit = Uint128(120000000000), + ), + ) + val declareTransactionPayload = account.signDeclare( + contractDefinition, + contractCasmDefinition, + params, + false, + ) + val request = provider.declareContract(declareTransactionPayload) + val result = request.send() - val receipt = provider.getTransactionReceipt(response.transactionHash).send() + val receipt = provider.getTransactionReceipt(result.transactionHash).send() assertTrue(receipt.isAccepted) } @@ -384,166 +452,321 @@ class StandardAccountTest { } } - @Test - fun `execute single call`() { - val call = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10)), - ) + @Nested + inner class InvokeTest { + @Test + fun `sign single call`() { + val call = Call( + contractAddress = balanceContractAddress, + calldata = listOf(Felt(10)), + entrypoint = "increase_balance", + ) - val result = account.execute(call).send() + val params = ExecutionParams( + maxFee = Felt(1000000000000000), + nonce = account.getNonce().send(), + ) - val receipt = provider.getTransactionReceipt(result.transactionHash).send() + val payload = account.sign(call, params) + val request = provider.invokeFunction(payload) + val response = request.send() - assertTrue(receipt.isAccepted) - } + val receipt = provider.getTransactionReceipt(response.transactionHash).send() - @Test - fun `execute single call with specific fee`() { - // Note to future developers experiencing failures in this test: - // This transaction may fail if the fee is too low. - val call = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10)), - ) + assertTrue(receipt.isAccepted) + } - val maxFee = Felt(10000000000000000L) - val result = account.execute(call, maxFee).send() + @Test + fun `sign v3 single call`() { + val call = Call( + contractAddress = balanceContractAddress, + calldata = listOf(Felt(10)), + entrypoint = "increase_balance", + ) - val receipt = provider.getTransactionReceipt(result.transactionHash).send() + val params = ExecutionParamsV3( + nonce = account.getNonce().send(), + l1ResourceBounds = ResourceBounds( + maxAmount = Uint64(20000), + maxPricePerUnit = Uint128(120000000000), + ), + ) - assertTrue(receipt.isAccepted) - assertNotEquals(Felt.ZERO, receipt.actualFee!!) - // TODO: re-enable this once devnet-rs is fixed - // assertTrue(receipt.actualFee!! < maxFee) - } + val payload = account.sign(call, params) + val request = provider.invokeFunction(payload) + val response = request.send() - @Test - fun `sign multiple calls test`() { - val call = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10)), - ) + val receipt = provider.getTransactionReceipt(response.transactionHash).send() - val params = ExecutionParams( - maxFee = Felt(1000000000000000), - nonce = account.getNonce().send(), - ) + assertTrue(receipt.isAccepted) + } - val payload = account.sign(listOf(call, call, call), params) - val response = provider.invokeFunction(payload).send() + @Test + fun `execute single call`() { + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) - val receipt = provider.getTransactionReceipt(response.transactionHash).send() + val result = account.execute(call).send() - assertTrue(receipt.isAccepted) - } + val receipt = provider.getTransactionReceipt(result.transactionHash).send() - @Test - fun `execute multiple calls`() { - val call1 = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10)), - ) + assertTrue(receipt.isAccepted) + } - val call2 = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10)), - ) + @Test + fun `execute v3 single call`() { + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) - val result = account.execute(listOf(call1, call2)).send() + val result = account.executeV3(call).send() - val receipt = provider.getTransactionReceipt(result.transactionHash).send() + val receipt = provider.getTransactionReceipt(result.transactionHash).send() - assertTrue(receipt.isAccepted) - } + assertTrue(receipt.isAccepted) + } - @Test - fun `two executes with single call`() { - val call = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10)), - ) + @Test + fun `execute single call with specific fee`() { + // Note to future developers experiencing failures in this test: + // This transaction may fail if the fee is too low. + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) - val result = account.execute(call).send() + val maxFee = Felt(10000000000000000L) + val result = account.execute(call, maxFee).send() - val receipt = provider.getTransactionReceipt(result.transactionHash).send() - assertTrue(receipt.isAccepted) + val receipt = provider.getTransactionReceipt(result.transactionHash).send() - val call2 = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(20)), - ) + assertTrue(receipt.isAccepted) + assertNotEquals(Felt.ZERO, receipt.actualFee) + } - val result2 = account.execute(call2).send() + @Test + fun `execute v3 single call with specific resource bounds`() { + // Note to future developers experiencing failures in this test: + // This transaction may fail if resource bounds are too low. + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) - val receipt2 = provider.getTransactionReceipt(result2.transactionHash).send() - assertTrue(receipt2.isAccepted) - } + val l1ResourceBounds = ResourceBounds( + maxAmount = Uint64(20000), + maxPricePerUnit = Uint128(120000000000), + ) + val result = account.executeV3(call, l1ResourceBounds).send() - @Test - fun `cairo1 account calldata`() { - val call1 = Call( - contractAddress = balanceContractAddress, - entrypoint = "increase_balance", - calldata = listOf(Felt(10), Felt(20), Felt(30)), - ) + val receipt = provider.getTransactionReceipt(result.transactionHash).send() - val call2 = Call( - contractAddress = Felt(999), - entrypoint = "empty_calldata", - calldata = listOf(), - ) + assertTrue(receipt.isAccepted) + assertNotEquals(Felt.ZERO, receipt.actualFee) + } - val call3 = Call( - contractAddress = Felt(123), - entrypoint = "another_method", - calldata = listOf(Felt(100), Felt(200)), - ) + @Test + fun `sign multiple calls test`() { + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) - val account = StandardAccount( - address = accountAddress, - signer = signer, - provider = provider, - cairoVersion = Felt.ONE, - ) - val params = ExecutionParams(Felt.ZERO, Felt.ZERO) - val signedTx = account.sign(listOf(call1, call2, call3), params) - - val expectedCalldata = listOf( - Felt(3), - balanceContractAddress, - selectorFromName("increase_balance"), - Felt(3), - Felt(10), - Felt(20), - Felt(30), - Felt(999), - selectorFromName("empty_calldata"), - Felt(0), - Felt(123), - selectorFromName("another_method"), - Felt(2), - Felt(100), - Felt(200), - ) - - assertEquals(expectedCalldata, signedTx.calldata) - - val signedEmptyTx = account.sign(listOf(), params) - - assertEquals(listOf(Felt.ZERO), signedEmptyTx.calldata) + val params = ExecutionParams( + maxFee = Felt(1000000000000000), + nonce = account.getNonce().send(), + ) + + val payload = account.sign(listOf(call, call, call), params) + val response = provider.invokeFunction(payload).send() + + val receipt = provider.getTransactionReceipt(response.transactionHash).send() + + assertTrue(receipt.isAccepted) + } + + @Test + fun `sign v3 multiple calls test`() { + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + + val params = ExecutionParamsV3( + nonce = account.getNonce().send(), + l1ResourceBounds = ResourceBounds( + maxAmount = Uint64(20000), + maxPricePerUnit = Uint128(120000000000), + ), + ) + + val payload = account.sign(listOf(call, call, call), params) + val response = provider.invokeFunction(payload).send() + + val receipt = provider.getTransactionReceipt(response.transactionHash).send() + + assertTrue(receipt.isAccepted) + } + + @Test + fun `execute multiple calls`() { + val call1 = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + + val call2 = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + + val result = account.execute(listOf(call1, call2)).send() + + val receipt = provider.getTransactionReceipt(result.transactionHash).send() + + assertTrue(receipt.isAccepted) + } + + @Test + fun `execute v3 multiple calls`() { + val call1 = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + + val call2 = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + + val result = account.executeV3(listOf(call1, call2)).send() + + val receipt = provider.getTransactionReceipt(result.transactionHash).send() + + assertTrue(receipt.isAccepted) + } + + @Test + fun `two executes with single call`() { + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + + val result = account.execute(call).send() + + val receipt = provider.getTransactionReceipt(result.transactionHash).send() + assertTrue(receipt.isAccepted) + + val call2 = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(20)), + ) + + val result2 = account.execute(call2).send() + + val receipt2 = provider.getTransactionReceipt(result2.transactionHash).send() + assertTrue(receipt2.isAccepted) + } + + @Test + fun `two executes v3 with single call`() { + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + + val result = account.executeV3(call).send() + + val receipt = provider.getTransactionReceipt(result.transactionHash).send() + assertTrue(receipt.isAccepted) + + val call2 = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(20)), + ) + + val result2 = account.executeV3(call2).send() + + val receipt2 = provider.getTransactionReceipt(result2.transactionHash).send() + assertTrue(receipt2.isAccepted) + } + + @Test + fun `cairo1 account calldata`() { + val call1 = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10), Felt(20), Felt(30)), + ) + + val call2 = Call( + contractAddress = Felt(999), + entrypoint = "empty_calldata", + calldata = listOf(), + ) + + val call3 = Call( + contractAddress = Felt(123), + entrypoint = "another_method", + calldata = listOf(Felt(100), Felt(200)), + ) + + val account = StandardAccount( + address = accountAddress, + signer = signer, + provider = provider, + cairoVersion = Felt.ONE, + ) + val params = ExecutionParams(Felt.ZERO, Felt.ZERO) + val signedTx = account.sign(listOf(call1, call2, call3), params) + + val expectedCalldata = listOf( + Felt(3), + balanceContractAddress, + selectorFromName("increase_balance"), + Felt(3), + Felt(10), + Felt(20), + Felt(30), + Felt(999), + selectorFromName("empty_calldata"), + Felt(0), + Felt(123), + selectorFromName("another_method"), + Felt(2), + Felt(100), + Felt(200), + ) + + assertEquals(expectedCalldata, signedTx.calldata) + + val signedEmptyTx = account.sign(listOf(), params) + + assertEquals(listOf(Felt.ZERO), signedEmptyTx.calldata) + } } @Test - fun `estimate deploy account fee`() { - val privateKey = Felt(11111) + fun `estimate deploy account v1 fee`() { + val privateKey = Felt(11112) val publicKey = StarknetCurve.getPublicKey(privateKey) val salt = Felt.ONE @@ -577,7 +800,40 @@ class StandardAccountTest { } @Test - fun `deploy account`() { + fun `estimate deploy account v3 fee`() { + val privateKey = Felt(22223) + val publicKey = StarknetCurve.getPublicKey(privateKey) + + val salt = Felt(2) + val calldata = listOf(publicKey) + val address = ContractAddressCalculator.calculateAddressFromHash( + classHash = accountContractClassHash, + calldata = calldata, + salt = salt, + ) + val account = StandardAccount( + address, + privateKey, + provider, + ) + val params = DeployAccountParamsV3( + nonce = Felt.ZERO, + l1ResourceBounds = ResourceBounds.ZERO, + ) + val payloadForFeeEstimation = account.signDeployAccount( + classHash = accountContractClassHash, + salt = salt, + calldata = calldata, + params = params, + forFeeEstimate = true, + ) + + val feePayload = provider.getEstimateFee(listOf(payloadForFeeEstimation)).send() + assertTrue(feePayload.first().overallFee.value > Felt.ONE.value) + } + + @Test + fun `sing and send deploy account v1 transaction`() { val privateKey = Felt(11111) val publicKey = StarknetCurve.getPublicKey(privateKey) @@ -588,7 +844,7 @@ class StandardAccountTest { calldata = calldata, salt = salt, ) - devnetClient.prefundAccount(address) + devnetClient.prefundAccountEth(address) val account = StandardAccount( address, @@ -609,7 +865,7 @@ class StandardAccountTest { assertEquals(address, response.address) // Make sure tx matches what we sent - val tx = provider.getTransaction(response.transactionHash).send() as DeployAccountTransaction + val tx = provider.getTransaction(response.transactionHash).send() as DeployAccountTransactionV1 assertEquals(payload.classHash, tx.classHash) assertEquals(payload.salt, tx.contractAddressSalt) assertEquals(payload.constructorCalldata, tx.constructorCalldata) @@ -631,6 +887,76 @@ class StandardAccountTest { assertTrue(receipt.isAccepted) } + @Test + fun `sign and send deploy account v3 transaction`() { + val privateKey = Felt(22222) + val publicKey = StarknetCurve.getPublicKey(privateKey) + + val salt = Felt(2) + val calldata = listOf(publicKey) + val address = ContractAddressCalculator.calculateAddressFromHash( + classHash = accountContractClassHash, + calldata = calldata, + salt = salt, + ) + + val newAccount = StandardAccount( + address, + privateKey, + provider, + ) + val l1ResourceBounds = ResourceBounds( + maxAmount = Uint64(20000), + maxPricePerUnit = Uint128(120000000000), + ) + val params = DeployAccountParamsV3( + nonce = Felt.ZERO, + l1ResourceBounds = l1ResourceBounds, + ) + + // Prefund the new account address with STRK + val transferCall = Call( + contractAddress = DevnetClient.strkErc20ContractAddress, + entrypoint = "transfer", + calldata = listOf(address) + l1ResourceBounds.toMaxFee().toUint256.toCalldata(), + ) + account.executeV3(transferCall).send() + + val payload = newAccount.signDeployAccount( + classHash = accountContractClassHash, + salt = salt, + calldata = calldata, + params = params, + forFeeEstimate = false, + ) + + val response = provider.deployAccount(payload).send() + + // Make sure the address matches the calculated one + assertEquals(address, response.address) + + // Make sure tx matches what we sent + val tx = provider.getTransaction(response.transactionHash).send() as DeployAccountTransactionV3 + assertEquals(payload.classHash, tx.classHash) + assertEquals(payload.salt, tx.contractAddressSalt) + assertEquals(payload.constructorCalldata, tx.constructorCalldata) + assertEquals(payload.version, tx.version) + assertEquals(payload.nonce, tx.nonce) + assertEquals(payload.signature, tx.signature) + + // Invoke function to make sure the account was deployed properly + val call = Call( + contractAddress = balanceContractAddress, + entrypoint = "increase_balance", + calldata = listOf(Felt(10)), + ) + val result = newAccount.executeV3(call).send() + + val receipt = provider.getTransactionReceipt(result.transactionHash).send() + + assertTrue(receipt.isAccepted) + } + @Test fun `send transaction signed for fee estimation`() { val privateKey = Felt(11111) @@ -670,7 +996,7 @@ class StandardAccountTest { @Test fun `simulate invoke and deploy account transactions`() { val account = StandardAccount(accountAddress, signer, provider) - devnetClient.prefundAccount(accountAddress) + devnetClient.prefundAccountEth(accountAddress) val nonce = account.getNonce().send() val call = Call( @@ -697,7 +1023,7 @@ class StandardAccountTest { privateKey, provider, ) - devnetClient.prefundAccount(newAccountAddress) + devnetClient.prefundAccountEth(newAccountAddress) val deployAccountTx = newAccount.signDeployAccount( classHash = accountContractClassHash, salt = salt, @@ -716,8 +1042,8 @@ class StandardAccountTest { assertTrue(simulationResult[0].transactionTrace is InvokeTransactionTrace) assertTrue(simulationResult[1].transactionTrace is DeployAccountTransactionTrace) - val invokeTxWithoutSignature = InvokeTransactionPayload(invokeTx.senderAddress, invokeTx.calldata, emptyList(), invokeTx.maxFee, invokeTx.version, invokeTx.nonce) - val deployAccountTxWithoutSignature = DeployAccountTransactionPayload(deployAccountTx.classHash, deployAccountTx.salt, deployAccountTx.constructorCalldata, deployAccountTx.version, deployAccountTx.nonce, deployAccountTx.maxFee, emptyList()) + val invokeTxWithoutSignature = InvokeTransactionV1Payload(invokeTx.senderAddress, invokeTx.calldata, emptyList(), invokeTx.maxFee, invokeTx.version, invokeTx.nonce) + val deployAccountTxWithoutSignature = DeployAccountTransactionV1Payload(deployAccountTx.classHash, deployAccountTx.salt, deployAccountTx.constructorCalldata, deployAccountTx.version, deployAccountTx.nonce, deployAccountTx.maxFee, emptyList()) val simulationFlags2 = setOf(SimulationFlag.SKIP_VALIDATE) val simulationResult2 = provider.simulateTransactions( diff --git a/lib/src/test/kotlin/starknet/crypto/FeeUtilsTest.kt b/lib/src/test/kotlin/starknet/crypto/FeeUtilsTest.kt index e52622440..41faa54b5 100644 --- a/lib/src/test/kotlin/starknet/crypto/FeeUtilsTest.kt +++ b/lib/src/test/kotlin/starknet/crypto/FeeUtilsTest.kt @@ -1,29 +1,74 @@ package starknet.crypto -import com.swmansion.starknet.account.estimatedFeeToMaxFee -import com.swmansion.starknet.data.types.Felt +import com.swmansion.starknet.data.types.* import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test class FeeUtilsTest { - @Test - fun `estimate fee to max fee - default`() { - val result = estimatedFeeToMaxFee(Felt(100)) - - assertEquals(result, Felt(110)) + companion object { + val estimateFee = EstimateFeeResponse( + gasConsumed = Felt(1000), + gasPrice = Felt(100), + overallFee = Felt(1000 * 100), + feeUnit = PriceUnit.WEI, + ) } - @Test - fun `estimate fee to max fee - 10 percent overhead`() { - val result = estimatedFeeToMaxFee(Felt(100), 0.50) + @Nested + inner class EstimateFeeToMaxFeeTest { + @Test + fun `estimate fee to max fee - default`() { + val result = estimateFee.toMaxFee() + + assertEquals(result, Felt(150000)) + } + + @Test + fun `estimate fee to max fee - specific overhead`() { + val result = estimateFee.toMaxFee(0.13) - assertEquals(result, Felt(150)) + assertEquals(result, Felt(113000)) + } + + @Test + fun `estimate fee to max fee - 0 overhead`() { + val result = estimateFee.toMaxFee(0.0) + + assertEquals(result, Felt(100000)) + } } - @Test - fun `estimate fee to max fee - 0 overhead`() { - val result = estimatedFeeToMaxFee(Felt(100), 0.0) + @Nested + inner class EstimateFeeToResourceBoundsTest { + @Test + fun `estimate fee to resource bounds - default`() { + val result = estimateFee.toResourceBounds() + val expected = ResourceBoundsMapping( + l1Gas = ResourceBounds(maxAmount = Uint64(1100), maxPricePerUnit = Uint128(150)), + l2Gas = ResourceBounds(maxAmount = Uint64.ZERO, maxPricePerUnit = Uint128.ZERO), + ) + assertEquals(expected, result) + } + + @Test + fun `estimate fee to resource bounds - specific overhead`() { + val result = estimateFee.toResourceBounds(0.19, 0.13) + val expected = ResourceBoundsMapping( + l1Gas = ResourceBounds(maxAmount = Uint64(1190), maxPricePerUnit = Uint128(113)), + l2Gas = ResourceBounds(maxAmount = Uint64.ZERO, maxPricePerUnit = Uint128.ZERO), + ) + assertEquals(expected, result) + } - assertEquals(result, Felt(100)) + @Test + fun `estimate fee to resource bounds - 0 overhead`() { + val result = estimateFee.toResourceBounds(0.0, 0.0) + val expected = ResourceBoundsMapping( + l1Gas = ResourceBounds(maxAmount = Uint64(1000), maxPricePerUnit = Uint128(100)), + l2Gas = ResourceBounds(maxAmount = Uint64.ZERO, maxPricePerUnit = Uint128.ZERO), + ) + assertEquals(expected, result) + } } } diff --git a/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt b/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt index 2720715e5..29088539c 100644 --- a/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt +++ b/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt @@ -38,7 +38,7 @@ internal class MerkleTreeTest { } @Nested - inner class BuildFromElementsTests { + inner class BuildFromElementsTest { @Test fun `build merkle tree from 1 element`() { val leaves = listOf(Felt.ONE) diff --git a/lib/src/test/kotlin/starknet/data/types/TransactionsTest.kt b/lib/src/test/kotlin/starknet/data/types/TransactionsTest.kt index acc4f81cc..b813a7622 100644 --- a/lib/src/test/kotlin/starknet/data/types/TransactionsTest.kt +++ b/lib/src/test/kotlin/starknet/data/types/TransactionsTest.kt @@ -13,18 +13,18 @@ import java.math.BigInteger internal class TransactionsTest { @Test fun getHash() { - val tx1 = TransactionFactory.makeInvokeTransaction( + val tx1 = TransactionFactory.makeInvokeV1Transaction( senderAddress = Felt.fromHex("0x2a"), calldata = listOf(), chainId = StarknetChainId.TESTNET, nonce = Felt.ZERO, maxFee = Felt.ZERO, - version = INVOKE_VERSION, + version = Felt.ONE, ) assertEquals(Felt.fromHex("0x22294fe217f962c39e4cb694a5db3f71e1132988451a9b2abc2d2ea8512088e"), tx1.hash) - val tx2 = TransactionFactory.makeInvokeTransaction( + val tx2 = TransactionFactory.makeInvokeV1Transaction( senderAddress = Felt( BigInteger("468485892896389608042320470922610020674017592380673471682128582128678525733"), ), @@ -49,7 +49,7 @@ internal class TransactionsTest { chainId = StarknetChainId.TESTNET, nonce = Felt.ZERO, maxFee = Felt(BigInteger("100000000")), - version = INVOKE_VERSION, + version = Felt.ONE, ) assertEquals(Felt.fromHex("0xff63c84949d3ab0ced753c227528493dea3dc4680c65c1facb7f86ae0472df"), tx2.hash) diff --git a/lib/src/test/kotlin/starknet/deployercontract/StandardDeployerTest.kt b/lib/src/test/kotlin/starknet/deployercontract/StandardDeployerTest.kt index 003d5d47f..c8f04462b 100644 --- a/lib/src/test/kotlin/starknet/deployercontract/StandardDeployerTest.kt +++ b/lib/src/test/kotlin/starknet/deployercontract/StandardDeployerTest.kt @@ -45,7 +45,7 @@ object StandardDeployerTest { balanceContractClassHash = devnetClient.declareContract("Balance").classHash // Prepare devnet address book - val accountDetails = devnetClient.createDeployAccount("standard_account_test").details + val accountDetails = devnetClient.deployAccount("standard_deployer_test", prefund = true).details signer = StarkCurveSigner(accountDetails.privateKey) accountAddress = accountDetails.address standardDeployer = StandardDeployer( diff --git a/lib/src/test/kotlin/starknet/provider/ProviderTest.kt b/lib/src/test/kotlin/starknet/provider/ProviderTest.kt index d9acbc35b..793f12925 100644 --- a/lib/src/test/kotlin/starknet/provider/ProviderTest.kt +++ b/lib/src/test/kotlin/starknet/provider/ProviderTest.kt @@ -3,7 +3,6 @@ package starknet.provider import com.swmansion.starknet.data.selectorFromName import com.swmansion.starknet.data.types.* import com.swmansion.starknet.data.types.transactions.* -import com.swmansion.starknet.provider.Provider import com.swmansion.starknet.provider.exceptions.RequestFailedException import com.swmansion.starknet.provider.exceptions.RpcRequestFailedException import com.swmansion.starknet.provider.rpc.JsonRpcProvider @@ -48,7 +47,7 @@ class ProviderTest { classHash = balanceClassHash, constructorCalldata = listOf(Felt(451)), ).contractAddress - deployAccountTransactionHash = devnetClient.createDeployAccount("provider_test").transactionHash + deployAccountTransactionHash = devnetClient.deployAccount("provider_test", prefund = true).transactionHash invokeTransactionHash = devnetClient.invokeContract( contractAddress = balanceContractAddress, function = "increase_balance", @@ -65,19 +64,6 @@ class ProviderTest { fun after() { devnetClient.close() } - - data class AddressBook( - val balanceContractAddress: Felt, - val balanceClassHash: Felt, - val invokeTransactionHash: Felt, - val declareTransactionHash: Felt, - val deployAccountTransactionHash: Felt, - ) - - data class ProviderParameters( - val provider: Provider, - val addressBook: AddressBook, - ) } @Test @@ -322,7 +308,10 @@ class ProviderTest { "jsonrpc": "2.0", "result": { - "actual_fee": "0x0", + "actual_fee": { + "amount": "0x244adfc7e22", + "unit": "WEI" + }, "block_hash": "0x4e782152c52c8637e03df60048deb4f6adf122ef37cf53eeb72322a4b9c9c52", "contract_address": "0x20f8c63faff27a0c5fe8a25dc1635c40c971bf67b8c35c6089a998649dfdfcb", "transaction_hash": "0x1a9d9e311ff31e27b20a7919bec6861dd6b603d72b7e8df9900cd7603200d0b", @@ -336,15 +325,15 @@ class ProviderTest { [], "execution_resources": { - "steps": "0x999", - "memory_holes": "0x1", - "range_check_builtin_applications": "0x21", - "pedersen_builtin_applications": "0x37", - "poseidon_builtin_applications": "0x451", - "ec_op_builtin_applications": "0x123", - "ecdsa_builtin_applications": "0x789", - "bitwise_builtin_applications": "0x0", - "keccak_builtin_applications": "0xd" + "steps": "999", + "memory_holes": "1", + "range_check_builtin_applications": "21", + "pedersen_builtin_applications": "37", + "poseidon_builtin_applications": "451", + "ec_op_builtin_applications": "123", + "ecdsa_builtin_applications": "789", + "bitwise_builtin_applications": "1", + "keccak_builtin_applications": "1" } } } @@ -385,22 +374,25 @@ class ProviderTest { "result": { "type": "INVOKE", "transaction_hash": "0x333198614194ae5b5ef921e63898a592de5e9f4d7b6e04745093da88b429f2a", - "actual_fee": "0x244adfc7e22", + "actual_fee": { + "amount": "0x244adfc7e22", + "unit": "FRI" + }, "messages_sent": [], "events": [], "execution_status": "SUCCEEDED", "finality_status": "ACCEPTED_ON_L2", "execution_resources": { - "steps": "0x999", - "memory_holes": "0x1", - "range_check_builtin_applications": "0x21", - "pedersen_builtin_applications": "0x37", - "poseidon_builtin_applications": "0x451", - "ec_op_builtin_applications": "0x123", - "ecdsa_builtin_applications": "0x789", - "bitwise_builtin_applications": "0x0", - "keccak_builtin_applications": "0xd" + "steps": "999", + "memory_holes": "1", + "range_check_builtin_applications": "21", + "pedersen_builtin_applications": "37", + "poseidon_builtin_applications": "451", + "ec_op_builtin_applications": "123", + "ecdsa_builtin_applications": "789", + "bitwise_builtin_applications": "1", + "keccak_builtin_applications": "1" } } } @@ -419,6 +411,7 @@ class ProviderTest { assertTrue(receipt is PendingTransactionReceipt) assertTrue(receipt is PendingInvokeTransactionReceipt) + assertEquals(PriceUnit.FRI, receipt.actualFee.unit) } @Test @@ -433,7 +426,10 @@ class ProviderTest { "jsonrpc": "2.0", "result": { "transaction_hash": "0x4b2ff971b669e31c704fde5c1ad6ee08ba2000986a25ad5106ab94546f36f7", - "actual_fee": "0x0", + "actual_fee": { + "amount": "0x244adfc7e22", + "unit": "WEI" + }, "finality_status": "ACCEPTED_ON_L2", "execution_status": "SUCCEEDED", "block_hash": "0x16c6bc59271e7b727ac0b139bbf99336fec1c0bfb6d41540d36fe1b3e2994c9", @@ -467,15 +463,15 @@ class ProviderTest { ], "execution_resources": { - "steps": "0x999", - "memory_holes": "0x1", - "range_check_builtin_applications": "0x21", - "pedersen_builtin_applications": "0x37", - "poseidon_builtin_applications": "0x451", - "ec_op_builtin_applications": "0x123", - "ecdsa_builtin_applications": "0x789", - "bitwise_builtin_applications": "0x0", - "keccak_builtin_applications": "0xd" + "steps": "999", + "memory_holes": "1", + "range_check_builtin_applications": "21", + "pedersen_builtin_applications": "37", + "poseidon_builtin_applications": "451", + "ec_op_builtin_applications": "123", + "ecdsa_builtin_applications": "789", + "bitwise_builtin_applications": "1", + "keccak_builtin_applications": "1" }, "message_hash": "0x8000000000000110000000000000000000000000000000000000011111111111" } @@ -490,6 +486,7 @@ class ProviderTest { assertNotNull(response) assertTrue(response is ProcessedTransactionReceipt) + assertEquals(PriceUnit.WEI, response.actualFee.unit) } @Test @@ -792,7 +789,7 @@ class ProviderTest { "l1_gas_price": { "price_in_wei": "0x2137", - "price_in_strk": "0x1234" + "price_in_fri": "0x1234" }, "starknet_version": "0.12.3", "transactions": [ @@ -874,9 +871,9 @@ class ProviderTest { "l1_gas_price": { "price_in_wei": "0x2137", - "price_in_strk": "0x1234" + "price_in_fri": "0x1234" }, - "starknet_version": "0.12.3", + "starknet_version": "0.13.0", "transactions": [ "0x01", "0x02" diff --git a/lib/src/test/kotlin/starknet/utils/DevnetClient.kt b/lib/src/test/kotlin/starknet/utils/DevnetClient.kt index 96091d5db..24aa1880e 100644 --- a/lib/src/test/kotlin/starknet/utils/DevnetClient.kt +++ b/lib/src/test/kotlin/starknet/utils/DevnetClient.kt @@ -2,6 +2,8 @@ package starknet.utils import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.StarknetChainId +import com.swmansion.starknet.data.types.transactions.TransactionExecutionStatus +import com.swmansion.starknet.data.types.transactions.TransactionStatus import com.swmansion.starknet.provider.Provider import com.swmansion.starknet.provider.rpc.JsonRpcProvider import com.swmansion.starknet.service.http.HttpService @@ -17,7 +19,6 @@ import java.nio.file.Paths import java.util.* import java.util.concurrent.TimeUnit import kotlin.io.path.absolutePathString -import kotlin.io.path.exists import kotlin.io.path.readText class DevnetClient( @@ -46,11 +47,15 @@ class DevnetClient( val provider: Provider by lazy { JsonRpcProvider(rpcUrl, StarknetChainId.TESTNET) } + private enum class TransactionVerificiationMode { RECEIPT, STATUS, DISABLED } + private val transactionVerificiationMode = TransactionVerificiationMode.STATUS + companion object { - // Source: https://github.com/0xSpaceShard/starknet-devnet-rs/blob/323f907bc3e3e4dc66b403ec6f8b58744e8d6f9a/crates/starknet/src/constants.rs + // Source: https://github.com/0xSpaceShard/starknet-devnet-rs/blob/85495efb71a37ad3921c8986474b7e78a9a9f5fc/crates/starknet/src/constants.rs val accountContractClassHash = Felt.fromHex("0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f") - val erc20ContractClassHash = Felt.fromHex("0x6a22bf63c7bc07effa39a25dfbd21523d211db0100a0afd054d172b81840eaf") - val erc20ContractAddress = Felt.fromHex("0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7") + val ethErc20ContractClassHash = Felt.fromHex("0x6a22bf63c7bc07effa39a25dfbd21523d211db0100a0afd054d172b81840eaf") + val ethErc20ContractAddress = Felt.fromHex("0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7") + val strkErc20ContractAddress = Felt.fromHex("0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d") val udcContractClassHash = Felt.fromHex("0x7b3e05f48f0c69e4a65ce5e076a66271a527aff2c34ce1083ec6e1526997a69") val udcContractAddress = Felt.fromHex("0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf") @@ -100,11 +105,14 @@ class DevnetClient( } isDevnetRunning = true - if (accountDirectory.exists()) { - accountDirectory.toFile().walkTopDown().forEach { it.delete() } - } + // TODO: Use the previous approach once sncast is updated to RPC 0.6.0 + // if (accountDirectory.exists()) { + // accountDirectory.toFile().walkTopDown().forEach { it.delete() } + // } + + // defaultAccountDetails = createDeployAccount("__default__").details - defaultAccountDetails = createDeployAccount("__default__").details + defaultAccountDetails = deployAccount("__default__", prefund = true).details } override fun close() { @@ -118,7 +126,7 @@ class DevnetClient( isDevnetRunning = false } - fun prefundAccount(accountAddress: Felt) { + fun prefundAccountEth(accountAddress: Felt) { val payload = HttpService.Payload( url = mintUrl, body = @@ -169,7 +177,13 @@ class DevnetClient( name: String, classHash: Felt = accountContractClassHash, maxFee: Felt = Felt(1000000000000000), + prefund: Boolean = false, + accountName: String = "__default__", ): DeployAccountResult { + if (prefund) { + prefundAccountEth(readAccountDetails(name).address) + } + val params = listOf( "deploy", "--name", @@ -182,9 +196,10 @@ class DevnetClient( val response = runSnCast( command = "account", args = params, + accountName = accountName, ) as AccountDeploySnCastResponse - requireTransactionSuccessful(response.transactionHash) + requireTransactionSuccessful(response.transactionHash, "Deploy Account") return DeployAccountResult( details = readAccountDetails(name), @@ -197,14 +212,14 @@ class DevnetClient( classHash: Felt = accountContractClassHash, salt: Felt? = null, maxFee: Felt = Felt(1000000000000000), + accountName: String = "__default__", ): DeployAccountResult { - val accountName = name ?: UUID.randomUUID().toString() - val createResult = createAccount(accountName, classHash, salt) + val newAccountName = name ?: UUID.randomUUID().toString() + val createResult = createAccount(newAccountName, classHash, salt) val details = createResult.details - prefundAccount(details.address) - val deployResult = deployAccount(accountName, classHash, maxFee) + val deployResult = deployAccount(newAccountName, classHash, maxFee, prefund = true, accountName) - requireTransactionSuccessful(deployResult.transactionHash) + requireTransactionSuccessful(deployResult.transactionHash, "Deploy Account") return DeployAccountResult( details = details, @@ -215,6 +230,7 @@ class DevnetClient( fun declareContract( contractName: String, maxFee: Felt = Felt(1000000000000000), + accountName: String = "__default__", ): DeclareContractResult { val params = listOf( "--contract-name", @@ -225,9 +241,10 @@ class DevnetClient( val response = runSnCast( command = "declare", args = params, + accountName = accountName, ) as DeclareSnCastResponse - requireTransactionSuccessful(response.transactionHash) + requireTransactionSuccessful(response.transactionHash, "Declare") return DeclareContractResult( classHash = response.classHash, @@ -241,6 +258,7 @@ class DevnetClient( salt: Felt? = null, unique: Boolean = false, maxFee: Felt = Felt(1000000000000000), + accountName: String = "__default__", ): DeployContractResult { val params = mutableListOf( "--class-hash", @@ -262,8 +280,11 @@ class DevnetClient( val response = runSnCast( command = "deploy", args = params, + accountName = accountName, ) as DeploySnCastResponse + requireTransactionSuccessful(response.transactionHash, "Deploy Contract") + return DeployContractResult( transactionHash = response.transactionHash, contractAddress = response.contractAddress, @@ -277,13 +298,12 @@ class DevnetClient( unique: Boolean = false, maxFeeDeclare: Felt = Felt(1000000000000000), maxFeeDeploy: Felt = Felt(1000000000000000), + accountName: String = "__default__", ): DeployContractResult { - val declareResponse = declareContract(contractName, maxFeeDeclare) - requireTransactionSuccessful(declareResponse.transactionHash) + val declareResponse = declareContract(contractName, maxFeeDeclare, accountName) val classHash = declareResponse.classHash - val deployResponse = deployContract(classHash, constructorCalldata, salt, unique, maxFeeDeploy) - requireTransactionSuccessful(deployResponse.transactionHash) + val deployResponse = deployContract(classHash, constructorCalldata, salt, unique, maxFeeDeploy, accountName) return DeployContractResult( transactionHash = deployResponse.transactionHash, @@ -296,6 +316,7 @@ class DevnetClient( function: String, calldata: List, maxFee: Felt = Felt(1000000000000000), + accountName: String = "__default__", ): InvokeContractResult { val params = mutableListOf( "--contract-address", @@ -312,16 +333,21 @@ class DevnetClient( val response = runSnCast( command = "invoke", args = params, + accountName = accountName, ) as InvokeSnCastResponse - requireTransactionSuccessful(response.transactionHash) + requireTransactionSuccessful(response.transactionHash, "Invoke") return InvokeContractResult( transactionHash = response.transactionHash, ) } - private fun runSnCast(command: String, args: List, accountName: String = "__default__"): SnCastResponse { + private fun runSnCast( + command: String, + args: List, + accountName: String = "__default__", + ): SnCastResponse { val processBuilder = ProcessBuilder( "sncast", "--json", @@ -366,12 +392,31 @@ class DevnetClient( return json.decodeFromString(AccountDetailsSerializer(accountName), contents) } - // This provides more info, but can be replaced with getTransactionStatus if there are changes to transaction receipts - private fun requireTransactionSuccessful(transactionHash: Felt) { + private fun requireTransactionSuccessful(transactionHash: Felt, type: String) { + // Receipt provides a more detailed error message than status, but is less reliable than getTransactionStatus + // Use status by default, use receipt for debugging purposes if needed + when (transactionVerificiationMode) { + TransactionVerificiationMode.RECEIPT -> requireTransactionReceiptSuccessful(transactionHash, type) + TransactionVerificiationMode.STATUS -> requireTransactionStatusSuccessful(transactionHash, type) + TransactionVerificiationMode.DISABLED -> {} + } + } + + private fun requireTransactionReceiptSuccessful(transactionHash: Felt, type: String) { val request = provider.getTransactionReceipt(transactionHash) val receipt = request.send() if (!receipt.isAccepted) { - throw DevnetTransactionFailedException("${receipt.type} transaction failed. Reason: ${receipt.revertReason}") + throw DevnetTransactionFailedException("$type transaction failed. Reason: ${receipt.revertReason}") + } + } + + private fun requireTransactionStatusSuccessful(transactionHash: Felt, type: String) { + val request = provider.getTransactionStatus(transactionHash) + val status = request.send() + if (status.executionStatus != TransactionExecutionStatus.SUCCEEDED && + (status.finalityStatus == TransactionStatus.ACCEPTED_ON_L1 || status.finalityStatus == TransactionStatus.ACCEPTED_ON_L2) + ) { + throw DevnetTransactionFailedException("$type transaction failed.") } } } diff --git a/lib/src/test/resources/accounts/provider_test/starknet_open_zeppelin_accounts.json b/lib/src/test/resources/accounts/provider_test/starknet_open_zeppelin_accounts.json new file mode 100644 index 000000000..8e485c51d --- /dev/null +++ b/lib/src/test/resources/accounts/provider_test/starknet_open_zeppelin_accounts.json @@ -0,0 +1,20 @@ +{ + "alpha-goerli": { + "__default__": { + "address": "0x10231652fd06fed78bb30d3353b87712d2eede34202d2ba851952e6d5c4574d", + "class_hash": "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f", + "deployed": true, + "private_key": "0xa2a170a20957bdc94beede6c8c7071fbe31b0d98ad4e74408927e3daf461ca", + "public_key": "0xcddf5b2c1aba5d3c095c4899e964318c8db2d391577d0ecef24cdcd4154031", + "salt": "0xe2a1ff6a5be76915" + }, + "provider_test": { + "address": "0x312713c69e79facabef2c621a2d25eed2d750a5f78e4fd713508f01a322e96f", + "class_hash": "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f", + "deployed": true, + "private_key": "0x1447c0a3a423daf43887fdfe1bfb981ef126214628bafaedce7cc73870860ad", + "public_key": "0x54049f964ef7bf61ebaa96316d93f15c38ec8511cbba085b5f22894ff6436f7", + "salt": "0xf7a8ad9230f702be" + } + } +} \ No newline at end of file diff --git a/lib/src/test/resources/accounts/standard_account_test/starknet_open_zeppelin_accounts.json b/lib/src/test/resources/accounts/standard_account_test/starknet_open_zeppelin_accounts.json new file mode 100644 index 000000000..e369bf9b5 --- /dev/null +++ b/lib/src/test/resources/accounts/standard_account_test/starknet_open_zeppelin_accounts.json @@ -0,0 +1,20 @@ +{ + "alpha-goerli": { + "__default__": { + "address": "0x236ca5856fe740e51f8ae1e9d3cdfeb6a27c32076022cf66cda2f13a54b5264", + "class_hash": "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f", + "deployed": true, + "private_key": "0x31355a5ff796ed563f07225409c644474a752d24a70966a58553b1ec2dceeb6", + "public_key": "0x565164759d34a2f18852f864671ec5400675de8ad69f430c46de5a3d413434f", + "salt": "0xbdea7f67a853ca92" + }, + "standard_account_test": { + "address": "0x4428a52af4b56b60eafba3bfe8d45f06b3ba6567db259e1f815f818632fd18f", + "class_hash": "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f", + "deployed": true, + "private_key": "0x458a49109147bfba58cace9907d5f1cb3c082056ea23ea5b305dcfe5c051c30", + "public_key": "0x5e469c845d0ad27f0109bd462fe05b35ae077914d0c889128410e8e0babe994", + "salt": "0x1a5dd1281a0f2a74" + } + } +} \ No newline at end of file diff --git a/lib/src/test/resources/accounts/standard_deployer_test/starknet_open_zeppelin_accounts.json b/lib/src/test/resources/accounts/standard_deployer_test/starknet_open_zeppelin_accounts.json new file mode 100644 index 000000000..45d23bbc6 --- /dev/null +++ b/lib/src/test/resources/accounts/standard_deployer_test/starknet_open_zeppelin_accounts.json @@ -0,0 +1,20 @@ +{ + "alpha-goerli": { + "__default__": { + "address": "0x6afe63e01a71f2e6dfdae23300d60728432e5760836c94a568c06a1b7c49868", + "class_hash": "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f", + "deployed": true, + "private_key": "0x2642371cff2281696e5cc7978ac154d6d07e1405b4d101ed49c54b9cf7e0415", + "public_key": "0x769afd8dc3a6ac5601908646eb30e8c1d8a972108ba8d2bb3a9c8620d457825", + "salt": "0x29c5bd48c16c7771" + }, + "standard_deployer_test": { + "address": "0x5b0fe31220be579b7c28158b0a89ffe4b08e6eb3953d55d0ad0905e10d90e93", + "class_hash": "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f", + "deployed": true, + "private_key": "0x417b9fd73fdd42a225ed669ae59306d4d7d6d489a4381fa6e71bdfb4887c520", + "public_key": "0x13cd1df69047336334bb41901cb48f69c9315106a73ec431f96ebb1bdecc91e", + "salt": "0xf0fd66a92dc871da" + } + } +} \ No newline at end of file