From 6ceb9e31e51d51a6ce374144fd3ddb82a6ea01b3 Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Mon, 4 Dec 2023 17:09:56 +0100 Subject: [PATCH] feat: hashes, signer, vip account, types, versions as string enum --- __tests__/account.test.ts | 5 +- .../utils/{hash.test.ts => classHash.test.ts} | 0 __tests__/utils/ellipticalCurve.test.ts | 11 +- __tests__/utils/stark.test.ts | 18 +- __tests__/utils/transactionHash.test.ts | 202 +++++++++++++++- __tests__/utils/v3hash.test.ts | 184 --------------- src/account/default.ts | 144 +++++++----- src/channel/rpc_0_6.ts | 3 +- src/index.ts | 3 +- src/signer/default.ts | 144 +++++++----- src/types/account.ts | 8 +- src/types/api/rpcspec_0_6/nonspec.ts | 27 +++ src/types/lib/index.ts | 10 +- src/types/signer.ts | 56 ++++- src/utils/{hash.ts => hash/classHash.ts} | 188 ++------------- src/utils/hash/index.ts | 8 + src/utils/hash/transactionHash/index.ts | 219 ++++++++++++++++++ src/utils/hash/transactionHash/v2.ts | 129 +++++++++++ .../{v3hash.ts => hash/transactionHash/v3.ts} | 16 +- src/utils/num.ts | 11 + src/utils/stark.ts | 45 +++- src/utils/transaction.ts | 21 ++ 22 files changed, 926 insertions(+), 526 deletions(-) rename __tests__/utils/{hash.test.ts => classHash.test.ts} (100%) delete mode 100644 __tests__/utils/v3hash.test.ts rename src/utils/{hash.ts => hash/classHash.ts} (60%) create mode 100644 src/utils/hash/index.ts create mode 100644 src/utils/hash/transactionHash/index.ts create mode 100644 src/utils/hash/transactionHash/v2.ts rename src/utils/{v3hash.ts => hash/transactionHash/v3.ts} (92%) diff --git a/__tests__/account.test.ts b/__tests__/account.test.ts index 04995e38b..c7a6fa90e 100644 --- a/__tests__/account.test.ts +++ b/__tests__/account.test.ts @@ -6,6 +6,7 @@ import { Provider, TransactionType, cairo, + constants, contractClassResponseToLegacyCompiledContract, ec, extractContractHashes, @@ -94,7 +95,9 @@ describe('deploy and test Wallet', () => { }); expect(result).toMatchSchemaRef('EstimateFee'); - expect(innerInvokeEstFeeSpy.mock.calls[0][1].version).toBe(hash.feeTransactionVersion); + expect(innerInvokeEstFeeSpy.mock.calls[0][1].version).toBe( + constants.BN_FEE_TRANSACTION_VERSION_1 + ); innerInvokeEstFeeSpy.mockClear(); }); diff --git a/__tests__/utils/hash.test.ts b/__tests__/utils/classHash.test.ts similarity index 100% rename from __tests__/utils/hash.test.ts rename to __tests__/utils/classHash.test.ts diff --git a/__tests__/utils/ellipticalCurve.test.ts b/__tests__/utils/ellipticalCurve.test.ts index f7c496c13..1b2492fdd 100644 --- a/__tests__/utils/ellipticalCurve.test.ts +++ b/__tests__/utils/ellipticalCurve.test.ts @@ -1,10 +1,7 @@ -import { ec } from '../../src'; +import { constants, ec } from '../../src'; import { StarknetChainId } from '../../src/constants'; -import { - calculateTransactionHash, - computeHashOnElements, - transactionVersion, -} from '../../src/utils/hash'; +import { computeHashOnElements } from '../../src/utils/hash'; +import { calculateTransactionHash } from '../../src/utils/hash/transactionHash/v2'; import { fromCallsToExecuteCalldataWithNonce } from '../../src/utils/transaction'; test('getKeyPair()', () => { @@ -53,7 +50,7 @@ test('hashMessage()', () => { const hashMsg = calculateTransactionHash( account, - transactionVersion, + constants.BN_TRANSACTION_VERSION_1, calldata, maxFee, StarknetChainId.SN_GOERLI, diff --git a/__tests__/utils/stark.test.ts b/__tests__/utils/stark.test.ts index b9a8303d4..63dddda97 100644 --- a/__tests__/utils/stark.test.ts +++ b/__tests__/utils/stark.test.ts @@ -1,4 +1,4 @@ -import { CallData, RawArgs, json, stark } from '../../src'; +import { CallData, EstimateFeeResponse, RawArgs, json, stark } from '../../src'; import { toBigInt, toHex } from '../../src/utils/num'; import { compiledOpenZeppelinAccount } from '../config/fixtures'; @@ -65,4 +65,20 @@ describe('stark', () => { expect(compiled).toEqual(['1', '2', '3', '10000000000', '4', '1', '2', '3', '4']); }); }); + + test('estimatedFeeToMaxFee', () => { + expect(stark.estimatedFeeToMaxFee(100)).toBe(150n); + }); + + test('estimateFeeToBounds', () => { + const estimateFeeResponse: EstimateFeeResponse = { + gas_consumed: 100n, + gas_price: 10n, + overall_fee: 1000n, + }; + expect(stark.estimateFeeToBounds(estimateFeeResponse)).toStrictEqual({ + l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + l1_gas: { max_amount: '0x6e', max_price_per_unit: '0xf' }, + }); + }); }); diff --git a/__tests__/utils/transactionHash.test.ts b/__tests__/utils/transactionHash.test.ts index 571205b92..40df46bd1 100644 --- a/__tests__/utils/transactionHash.test.ts +++ b/__tests__/utils/transactionHash.test.ts @@ -1,17 +1,199 @@ -import { StarknetChainId, TransactionHashPrefix } from '../../src/constants'; -import { calculateTransactionHashCommon } from '../../src/utils/hash'; +import { constants, hash, shortString, types, v2hash, v3hash } from '../../src'; +import { ResourceBounds } from '../../src/types/api/rpcspec_0_6'; -describe('calculateTransactionHashCommon()', () => { - test('should match most simple python output', () => { - const result = calculateTransactionHashCommon( - TransactionHashPrefix.INVOKE, +describe('TxV2 Hash Tests', () => { + describe('calculateTransactionHashCommon()', () => { + test('should match most simple python output', () => { + const result = v2hash.calculateTransactionHashCommon( + constants.TransactionHashPrefix.INVOKE, + '0x0', + '0x2a', + '0x64', + [], + '0x0', + constants.StarknetChainId.SN_GOERLI + ); + expect(result).toBe('0x7d260744de9d8c55e7675a34512d1951a7b262c79e685d26599edd2948de959'); + }); + }); +}); + +describe('TxV3 Hash Tests', () => { + test('DaMode', () => { + const result = v3hash.hashDAMode(types.RPC.EDAMode.L1, types.RPC.EDAMode.L1); + expect(result.toString(16)).toBe('0'); + + const result1 = v3hash.hashDAMode(types.RPC.EDAMode.L1, types.RPC.EDAMode.L2); + expect(result1.toString(16)).toBe('1'); + + const result2 = v3hash.hashDAMode(types.RPC.EDAMode.L2, types.RPC.EDAMode.L1); + expect(result2.toString(16)).toBe('100000000'); + + const result3 = v3hash.hashDAMode(types.RPC.EDAMode.L2, types.RPC.EDAMode.L2); + expect(result3.toString(16)).toBe('100000001'); + }); + + test('hashFeeField', () => { + const bound1: ResourceBounds = { + l2_gas: { + max_amount: '0', + max_price_per_unit: '0', + }, + l1_gas: { + max_amount: '0x7c9', + max_price_per_unit: '0x1', + }, + }; + const result1 = v3hash.hashFeeField(0, bound1); + expect(result1.toString(16)).toBe( + '7be65f04548dfe645c70f07d1f8ead572c09e0e6e125c47d4cc22b4de3597cc' + ); + }); + + test('calculateInvokeTransactionHash Demo', () => { + const result = hash.calculateInvokeTransactionHash({ + senderAddress: '0x12fd538', + version: '0x3', + compiledCalldata: ['0x11', '0x26'], + chainId: shortString.encodeShortString('1') as constants.StarknetChainId, + nonce: 9, + accountDeploymentData: [], + nonceDataAvailabilityMode: types.RPC.EDAMode.L1, + feeDataAvailabilityMode: types.RPC.EDAMode.L1, + resourceBounds: { + l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + l1_gas: { max_amount: '0x7c9', max_price_per_unit: '0x1' }, + }, + tip: 0, + paymasterData: [], + }); + + expect(result).toBe('0x35591624e5ea7e612f7c65f7c5fcfa0d972365359cfb611aaf93a13a6026a13'); + }); + + test('calculateInvokeTransactionHash Network', () => { + const result = v3hash.calculateInvokeTransactionHash( + '0x3f6f3bc663aedc5285d6013cc3ffcbc4341d86ab488b8b68d297f8258793c41', + '0x3', + [ + '0x2', + '0x4c312760dfd17a954cdd09e76aa9f149f806d88ec3e402ffaf5c4926f568a42', + '0x31aafc75f498fdfa7528880ad27246b4c15af4954f96228c9a132b328de1c92', + '0x0', + '0x6', + '0x450703c32370cf7ffff540b9352e7ee4ad583af143a361155f2b485c0c39684', + '0xb17d8a2731ba7ca1816631e6be14f0fc1b8390422d649fa27f0fbb0c91eea8', + '0x6', + '0x0', + '0x6', + '0x6333f10b24ed58cc33e9bac40b0d52e067e32a175a97ca9e2ce89fe2b002d82', + '0x3', + '0x602e89fe5703e5b093d13d0a81c9e6d213338dc15c59f4d3ff3542d1d7dfb7d', + '0x20d621301bea11ffd9108af1d65847e9049412159294d0883585d4ad43ad61b', + '0x276faadb842bfcbba834f3af948386a2eb694f7006e118ad6c80305791d3247', + '0x613816405e6334ab420e53d4b38a0451cb2ebca2755171315958c87d303cf6', + ], + constants.StarknetChainId.SN_GOERLI, + '0x8a9', + [], + 0, + 0, + { + l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + l1_gas: { max_amount: '0x186a0', max_price_per_unit: '0x5af3107a4000' }, + }, + '0x0', + [] + ); + + expect(result).toBe('0x41906f1c314cca5f43170ea75d3b1904196a10101190d2b12a41cc61cfd17c'); + }); + + test('calculateDeployAccountTransactionHash Demo', () => { + const result = v3hash.calculateDeployAccountTransactionHash( + '0x219bea54dc352c0d6853de34019644758620fa6298c4608829228c3f5f8db33', + '0x65bcf29c898ff912fa2bdd4c6cd94b9142da0399127601ef35dfc9babc7a691', + ['0x21b', '0x151'], + '0x12fd537', + '0x3', + shortString.encodeShortString('2') as constants.StarknetChainId, '0x0', - '0x2a', - '0x64', + types.RPC.EDAMode.L1, + types.RPC.EDAMode.L1, + { + l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + l1_gas: { max_amount: '0x7c9', max_price_per_unit: '0x1' }, + }, + '0x0', + [] + ); + + expect(result).toBe('0x3877e0ffb3917187deb3321f6017f5339d22a3753d498df76203b6b8120dde5'); + }); + + test('calculateDeployAccountTransactionHash Network', () => { + const result = v3hash.calculateDeployAccountTransactionHash( + '0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50', + '0x2338634f11772ea342365abd5be9d9dc8a6f44f159ad782fdebd3db5d969738', + ['0x5cd65f3d7daea6c63939d659b8473ea0c5cd81576035a4d34e52fb06840196c'], + '0x0', + '0x3', + constants.StarknetChainId.SN_GOERLI, + '0x0', + types.RPC.EDAMode.L1, + types.RPC.EDAMode.L1, + { + l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + l1_gas: { max_amount: '0x186a0', max_price_per_unit: '0x5af3107a4000' }, + }, + '0x0', + [] + ); + + expect(result).toBe('0x29fd7881f14380842414cdfdd8d6c0b1f2174f8916edcfeb1ede1eb26ac3ef0'); + }); + + test('calculateDeclareTransactionHash Demo', () => { + const result = v3hash.calculateDeclareTransactionHash( + '0x7d6b55b53dc0b621bb7e2b501340e4a88f7c448b513c9882d1be7ffac42ba3', + '0x7b', + '0x12fd538', + '0x3', + shortString.encodeShortString('3') as constants.StarknetChainId, + '0x0', + ['0x0'], + types.RPC.EDAMode.L1, + types.RPC.EDAMode.L1, + { + l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + l1_gas: { max_amount: '0x7c9', max_price_per_unit: '0x1' }, + }, + '0x0', + ['0x0'] + ); + + expect(result).toBe('0x6819909698b213a42e90751f85e6c6be877a679503e1a50921b1efc7ea997e'); + }); + + test('calculateDeclareTransactionHash Network', () => { + const result = v3hash.calculateDeclareTransactionHash( + '0x5ae9d09292a50ed48c5930904c880dab56e85b825022a7d689cfc9e65e01ee7', + '0x1add56d64bebf8140f3b8a38bdf102b7874437f0c861ab4ca7526ec33b4d0f8', + '0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50', + '0x3', + constants.StarknetChainId.SN_GOERLI, + '0x1', [], + types.RPC.EDAMode.L1, + types.RPC.EDAMode.L1, + { + l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + l1_gas: { max_amount: '0x186a0', max_price_per_unit: '0x2540be400' }, + }, '0x0', - StarknetChainId.SN_GOERLI + [] ); - expect(result).toBe('0x7d260744de9d8c55e7675a34512d1951a7b262c79e685d26599edd2948de959'); + + expect(result).toBe('0x41d1f5206ef58a443e7d3d1ca073171ec25fa75313394318fc83a074a6631c3'); }); }); diff --git a/__tests__/utils/v3hash.test.ts b/__tests__/utils/v3hash.test.ts deleted file mode 100644 index f4e59b8af..000000000 --- a/__tests__/utils/v3hash.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { types, v3hash } from '../../src'; -import { StarknetChainId } from '../../src/constants'; -import { ResourceBounds } from '../../src/types/api/rpcspec_0_6'; -import { encodeShortString } from '../../src/utils/shortString'; - -describe('TxV3 Hash Test', () => { - test('DaMode', () => { - const result = v3hash.hashDAMode(types.RPC.EDAMode.L1, types.RPC.EDAMode.L1); - expect(result.toString(16)).toBe('0'); - - const result1 = v3hash.hashDAMode(types.RPC.EDAMode.L1, types.RPC.EDAMode.L2); - expect(result1.toString(16)).toBe('1'); - - const result2 = v3hash.hashDAMode(types.RPC.EDAMode.L2, types.RPC.EDAMode.L1); - expect(result2.toString(16)).toBe('100000000'); - - const result3 = v3hash.hashDAMode(types.RPC.EDAMode.L2, types.RPC.EDAMode.L2); - expect(result3.toString(16)).toBe('100000001'); - }); - - test('hashFeeField', () => { - const bound1: ResourceBounds = { - l2_gas: { - max_amount: '0', - max_price_per_unit: '0', - }, - l1_gas: { - max_amount: '0x7c9', - max_price_per_unit: '0x1', - }, - }; - const result1 = v3hash.hashFeeField(0, bound1); - expect(result1.toString(16)).toBe( - '7be65f04548dfe645c70f07d1f8ead572c09e0e6e125c47d4cc22b4de3597cc' - ); - }); - - test('calculateInvokeTransactionHash Demo', () => { - const result = v3hash.calculateInvokeTransactionHash( - '0x12fd538', - '0x3', - ['0x11', '0x26'], - encodeShortString('1') as StarknetChainId, - '0x9', - [], - types.RPC.EDAMode.L1, - types.RPC.EDAMode.L1, - { - l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, - l1_gas: { max_amount: '0x7c9', max_price_per_unit: '0x1' }, - }, - '0x0', - [] - ); - - expect(result).toBe('0x35591624e5ea7e612f7c65f7c5fcfa0d972365359cfb611aaf93a13a6026a13'); - }); - - test('calculateInvokeTransactionHash Network', () => { - const result = v3hash.calculateInvokeTransactionHash( - '0x3f6f3bc663aedc5285d6013cc3ffcbc4341d86ab488b8b68d297f8258793c41', - '0x3', - [ - '0x2', - '0x4c312760dfd17a954cdd09e76aa9f149f806d88ec3e402ffaf5c4926f568a42', - '0x31aafc75f498fdfa7528880ad27246b4c15af4954f96228c9a132b328de1c92', - '0x0', - '0x6', - '0x450703c32370cf7ffff540b9352e7ee4ad583af143a361155f2b485c0c39684', - '0xb17d8a2731ba7ca1816631e6be14f0fc1b8390422d649fa27f0fbb0c91eea8', - '0x6', - '0x0', - '0x6', - '0x6333f10b24ed58cc33e9bac40b0d52e067e32a175a97ca9e2ce89fe2b002d82', - '0x3', - '0x602e89fe5703e5b093d13d0a81c9e6d213338dc15c59f4d3ff3542d1d7dfb7d', - '0x20d621301bea11ffd9108af1d65847e9049412159294d0883585d4ad43ad61b', - '0x276faadb842bfcbba834f3af948386a2eb694f7006e118ad6c80305791d3247', - '0x613816405e6334ab420e53d4b38a0451cb2ebca2755171315958c87d303cf6', - ], - StarknetChainId.SN_GOERLI, - '0x8a9', - [], - 0, - 0, - { - l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, - l1_gas: { max_amount: '0x186a0', max_price_per_unit: '0x5af3107a4000' }, - }, - '0x0', - [] - ); - - expect(result).toBe('0x41906f1c314cca5f43170ea75d3b1904196a10101190d2b12a41cc61cfd17c'); - }); - - test('calculateDeployAccountTransactionHash Demo', () => { - const result = v3hash.calculateDeployAccountTransactionHash( - '0x219bea54dc352c0d6853de34019644758620fa6298c4608829228c3f5f8db33', - '0x65bcf29c898ff912fa2bdd4c6cd94b9142da0399127601ef35dfc9babc7a691', - ['0x21b', '0x151'], - '0x12fd537', - '0x3', - encodeShortString('2') as StarknetChainId, - '0x0', - types.RPC.EDAMode.L1, - types.RPC.EDAMode.L1, - { - l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, - l1_gas: { max_amount: '0x7c9', max_price_per_unit: '0x1' }, - }, - '0x0', - [] - ); - - expect(result).toBe('0x3877e0ffb3917187deb3321f6017f5339d22a3753d498df76203b6b8120dde5'); - }); - - test('calculateDeployAccountTransactionHash Network', () => { - const result = v3hash.calculateDeployAccountTransactionHash( - '0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50', - '0x2338634f11772ea342365abd5be9d9dc8a6f44f159ad782fdebd3db5d969738', - ['0x5cd65f3d7daea6c63939d659b8473ea0c5cd81576035a4d34e52fb06840196c'], - '0x0', - '0x3', - StarknetChainId.SN_GOERLI, - '0x0', - types.RPC.EDAMode.L1, - types.RPC.EDAMode.L1, - { - l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, - l1_gas: { max_amount: '0x186a0', max_price_per_unit: '0x5af3107a4000' }, - }, - '0x0', - [] - ); - - expect(result).toBe('0x29fd7881f14380842414cdfdd8d6c0b1f2174f8916edcfeb1ede1eb26ac3ef0'); - }); - - test('calculateDeclareTransactionHash Demo', () => { - const result = v3hash.calculateDeclareTransactionHash( - '0x7d6b55b53dc0b621bb7e2b501340e4a88f7c448b513c9882d1be7ffac42ba3', - '0x7b', - '0x12fd538', - '0x3', - encodeShortString('3') as StarknetChainId, - '0x0', - ['0x0'], - types.RPC.EDAMode.L1, - types.RPC.EDAMode.L1, - { - l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, - l1_gas: { max_amount: '0x7c9', max_price_per_unit: '0x1' }, - }, - '0x0', - ['0x0'] - ); - - expect(result).toBe('0x6819909698b213a42e90751f85e6c6be877a679503e1a50921b1efc7ea997e'); - }); - - test('calculateDeclareTransactionHash Network', () => { - const result = v3hash.calculateDeclareTransactionHash( - '0x5ae9d09292a50ed48c5930904c880dab56e85b825022a7d689cfc9e65e01ee7', - '0x1add56d64bebf8140f3b8a38bdf102b7874437f0c861ab4ca7526ec33b4d0f8', - '0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50', - '0x3', - StarknetChainId.SN_GOERLI, - '0x1', - [], - types.RPC.EDAMode.L1, - types.RPC.EDAMode.L1, - { - l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, - l1_gas: { max_amount: '0x186a0', max_price_per_unit: '0x2540be400' }, - }, - '0x0', - [] - ); - - expect(result).toBe('0x41d1f5206ef58a443e7d3d1ca073171ec25fa75313394318fc83a074a6631c3'); - }); -}); diff --git a/src/account/default.ts b/src/account/default.ts index d4bb0fa66..770edad2c 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -21,7 +21,6 @@ import { DeployContractResponse, DeployContractUDCResponse, DeployTransactionReceiptResponse, - Details, EstimateFee, EstimateFeeAction, EstimateFeeBulk, @@ -41,20 +40,25 @@ import { TypedData, UniversalDeployerContractPayload, } from '../types'; +import { + EDataAvailabilityMode, + ETransactionVersion, + ETransactionVersion2, + ETransactionVersion3, +} from '../types/api/rpc'; import { CallData } from '../utils/calldata'; import { extractContractHashes, isSierra } from '../utils/contract'; import { starkCurve } from '../utils/ec'; import { parseUDCEvent } from '../utils/events'; -import { - calculateContractAddressFromHash, - feeTransactionVersion, - feeTransactionVersion_2, - transactionVersion, - transactionVersion_2, -} from '../utils/hash'; +import { calculateContractAddressFromHash } from '../utils/hash'; import { toBigInt, toCairoBool } from '../utils/num'; import { parseContract } from '../utils/provider'; -import { estimatedFeeToMaxFee, formatSignature, randomAddress } from '../utils/stark'; +import { + estimateFeeToBounds, + estimatedFeeToMaxFee, + formatSignature, + randomAddress, +} from '../utils/stark'; import { getExecuteCalldata } from '../utils/transaction'; import { getMessageHash } from '../utils/typedData'; import { AccountInterface } from './interface'; @@ -124,7 +128,7 @@ export class Account extends Provider implements AccountInterface { ): Promise { const transactions = Array.isArray(calls) ? calls : [calls]; const nonce = toBigInt(providedNonce ?? (await this.getNonce())); - const version = toBigInt(feeTransactionVersion); + const version = ETransactionVersion.F1; const chainId = await this.getChainId(); const signerDetails: InvocationsSignerDetails = { @@ -156,7 +160,7 @@ export class Account extends Provider implements AccountInterface { { blockIdentifier, nonce: providedNonce }: EstimateFeeDetails = {} ): Promise { const nonce = toBigInt(providedNonce ?? (await this.getNonce())); - const version = !isSierra(contract) ? feeTransactionVersion : feeTransactionVersion_2; + const version = !isSierra(contract) ? ETransactionVersion.F1 : ETransactionVersion.F2; const chainId = await this.getChainId(); const declareContractTransaction = await this.buildDeclarePayload( @@ -193,7 +197,7 @@ export class Account extends Provider implements AccountInterface { }: DeployAccountContractPayload, { blockIdentifier }: EstimateFeeDetails = {} ): Promise { - const version = toBigInt(feeTransactionVersion); + const version = ETransactionVersion.F1; const nonce = ZERO; // DEPLOY_ACCOUNT transaction will have a nonce zero as it is the first transaction in the account const chainId = await this.getChainId(); @@ -205,7 +209,7 @@ export class Account extends Provider implements AccountInterface { version, walletAddress: this.address, // unused parameter maxFee: ZERO, - cairoVersion: undefined, // unused parameter + cairoVersion: undefined, // unused parameter, } ); @@ -235,7 +239,7 @@ export class Account extends Provider implements AccountInterface { { nonce, blockIdentifier }: EstimateFeeDetails = {} ): Promise { const accountInvocations = await this.accountInvocationsFactory(invocations, { - versions: [feeTransactionVersion, feeTransactionVersion_2], + versions: [ETransactionVersion.F1, ETransactionVersion.F2], nonce, blockIdentifier, }); @@ -280,7 +284,7 @@ export class Account extends Provider implements AccountInterface { { type: TransactionType.INVOKE, payload: calls }, transactionsDetail )); - const version = toBigInt(transactionVersion); + const version = ETransactionVersion.V1; const chainId = await this.getChainId(); const signerDetails: InvocationsSignerDetails = { @@ -333,28 +337,30 @@ export class Account extends Provider implements AccountInterface { transactionsDetail: InvocationsDetails = {} ): Promise { const declareContractPayload = extractContractHashes(payload); - const details = {} as Details; - - details.nonce = toBigInt(transactionsDetail.nonce ?? (await this.getNonce())); - details.maxFee = - transactionsDetail.maxFee ?? - (await this.getSuggestedMaxFee( - { - type: TransactionType.DECLARE, - payload: declareContractPayload, - }, - transactionsDetail - )); - details.version = !isSierra(payload.contract) ? transactionVersion : transactionVersion_2; - details.chainId = await this.getChainId(); - const declareContractTransaction = await this.buildDeclarePayload(declareContractPayload, { - ...details, + const declareDetails: InvocationsSignerDetails = { + nonce: toBigInt(transactionsDetail.nonce ?? (await this.getNonce())), + maxFee: + transactionsDetail.maxFee ?? + (await this.getSuggestedMaxFee( + { + type: TransactionType.DECLARE, + payload: declareContractPayload, + }, + transactionsDetail + )), + version: !isSierra(payload.contract) ? ETransactionVersion.V1 : ETransactionVersion.V2, + chainId: await this.getChainId(), walletAddress: this.address, - cairoVersion: undefined, // unused parameter - }); + cairoVersion: undefined, + }; + + const declareContractTransaction = await this.buildDeclarePayload( + declareContractPayload, + declareDetails + ); - return this.declareContract(declareContractTransaction, details); + return this.declareContract(declareContractTransaction, declareDetails); } public async deploy( @@ -440,7 +446,7 @@ export class Account extends Provider implements AccountInterface { }: DeployAccountContractPayload, transactionsDetail: InvocationsDetails = {} ): Promise { - const version = toBigInt(transactionVersion); + const version = ETransactionVersion.V1; const nonce = ZERO; // DEPLOY_ACCOUNT transaction will have a nonce zero as it is the first transaction in the account const chainId = await this.getChainId(); @@ -550,22 +556,27 @@ export class Account extends Provider implements AccountInterface { */ public async buildDeclarePayload( payload: DeclareContractPayload, - { nonce, chainId, version, walletAddress, maxFee }: InvocationsSignerDetails + details: InvocationsSignerDetails ): Promise { const { classHash, contract, compiledClassHash } = extractContractHashes(payload); const compressedCompiledContract = parseContract(contract); + + if ( + typeof compiledClassHash === 'undefined' && + Object.values(ETransactionVersion3).includes(details.version as any) + ) { + throw Error('V3 Transaction work with Cairo1 Contracts and require compiledClassHash'); + } + const signature = await this.signer.signDeclareTransaction({ + ...details, classHash, - compiledClassHash, - senderAddress: walletAddress, - chainId, - maxFee, - version, - nonce, + compiledClassHash: compiledClassHash as string, // TODO: TS Nekuzi da v2 nemora imat a v3 mora i da je throvano ako nije definiran + senderAddress: details.walletAddress, }); return { - senderAddress: walletAddress, + senderAddress: details.walletAddress, signature, contract: compressedCompiledContract, compiledClassHash, @@ -579,7 +590,7 @@ export class Account extends Provider implements AccountInterface { constructorCalldata = [], contractAddress: providedContractAddress, }: DeployAccountContractPayload, - { nonce, chainId, version, maxFee }: InvocationsSignerDetails + details: InvocationsSignerDetails ): Promise { const compiledCalldata = CallData.compile(constructorCalldata); const contractAddress = @@ -587,12 +598,9 @@ export class Account extends Provider implements AccountInterface { calculateContractAddressFromHash(addressSalt, classHash, compiledCalldata, 0); const signature = await this.signer.signDeployAccountTransaction({ + ...details, classHash, contractAddress, - chainId, - maxFee, - version, - nonce, addressSalt, constructorCalldata: compiledCalldata, }); @@ -637,7 +645,7 @@ export class Account extends Provider implements AccountInterface { { nonce, blockIdentifier, skipValidate, skipExecute }: SimulateTransactionDetails = {} ): Promise { const accountInvocations = await this.accountInvocationsFactory(invocations, { - versions: [transactionVersion, transactionVersion_2], + versions: [ETransactionVersion.V1, ETransactionVersion.V2], nonce, blockIdentifier, }); @@ -667,14 +675,32 @@ export class Account extends Provider implements AccountInterface { return Promise.all( ([] as Invocations).concat(invocations).map(async (transaction, index: number) => { const txPayload: any = 'payload' in transaction ? transaction.payload : transaction; - const signerDetails: InvocationsSignerDetails = { - walletAddress: this.address, - nonce: toBigInt(Number(safeNonce) + index), - maxFee: ZERO, - version, - chainId, - cairoVersion, - }; + let signerDetails: InvocationsSignerDetails; + if (Object.values(ETransactionVersion2).includes(version as any)) { + signerDetails = { + walletAddress: this.address, + nonce: toBigInt(Number(safeNonce) + index), + maxFee: ZERO, + version: version as ETransactionVersion2, + chainId, + cairoVersion, + }; + } else if (Object.values(ETransactionVersion3).includes(version as any)) { + signerDetails = { + walletAddress: this.address, + nonce: toBigInt(Number(safeNonce) + index), + version: version as ETransactionVersion3, + chainId, + cairoVersion, + resourceBounds: estimateFeeToBounds(ZERO), + // TODO: this is defaults also inherit data from params + tip: 0, + paymasterData: [], + accountDeploymentData: [], + nonceDataAvailabilityMode: EDataAvailabilityMode.L1, + feeDataAvailabilityMode: EDataAvailabilityMode.L1, + }; + } else throw Error('un-supported version'); const common = { type: transaction.type, version, @@ -693,9 +719,7 @@ export class Account extends Provider implements AccountInterface { } as AccountInvocationItem; } if (transaction.type === TransactionType.DECLARE) { - signerDetails.version = !isSierra(txPayload.contract) - ? toBigInt(versions[0]) - : toBigInt(versions[1]); + signerDetails.version = !isSierra(txPayload.contract) ? versions[0] : versions[1]; const payload = await this.buildDeclarePayload(txPayload, signerDetails); return { ...common, diff --git a/src/channel/rpc_0_6.ts b/src/channel/rpc_0_6.ts index 1b22e4609..73ab257b5 100644 --- a/src/channel/rpc_0_6.ts +++ b/src/channel/rpc_0_6.ts @@ -27,11 +27,12 @@ import { import { CallData } from '../utils/calldata'; import { isSierra } from '../utils/contract'; import fetch from '../utils/fetchPonyfill'; -import { getSelector, getSelectorFromName, getVersionsByType } from '../utils/hash'; +import { getSelector, getSelectorFromName } from '../utils/hash'; import { stringify } from '../utils/json'; import { getHexStringArray, toHex, toStorageKey } from '../utils/num'; import { Block, getDefaultNodeUrl, isV3Tx, wait } from '../utils/provider'; import { decompressProgram, signatureToHexArray } from '../utils/stark'; +import { getVersionsByType } from '../utils/transaction'; /* function detailsToV3DefaultDetails(details: InvocationsDetailsWithNonce) { if (!isV3Tx(details)) throw Error('detailsToV3Details: Transaction is not V3'); diff --git a/src/index.ts b/src/index.ts index 4008b86a4..5d9d5e4d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,8 @@ export * as types from './types'; export * as constants from './constants'; export * as encode from './utils/encode'; export * as hash from './utils/hash'; -export * as v3hash from './utils/v3hash'; +export * as v3hash from './utils/hash/transactionHash/v3'; +export * as v2hash from './utils/hash/transactionHash/v2'; export * as json from './utils/json'; export * as num from './utils/num'; export * as transaction from './utils/transaction'; diff --git a/src/signer/default.ts b/src/signer/default.ts index 353dd0c45..8d6645fdd 100644 --- a/src/signer/default.ts +++ b/src/signer/default.ts @@ -1,21 +1,28 @@ import { - Abi, Call, DeclareSignerDetails, DeployAccountSignerDetails, InvocationsSignerDetails, Signature, TypedData, + V2DeclareSignerDetails, + V2DeployAccountSignerDetails, + V2InvocationsSignerDetails, + V3DeclareSignerDetails, + V3DeployAccountSignerDetails, + V3InvocationsSignerDetails, } from '../types'; +import { ETransactionVersion2, ETransactionVersion3 } from '../types/api/rpc'; import { CallData } from '../utils/calldata'; import { starkCurve } from '../utils/ec'; import { buf2hex } from '../utils/encode'; import { calculateDeclareTransactionHash, calculateDeployAccountTransactionHash, - calculateTransactionHash, + calculateInvokeTransactionHash, } from '../utils/hash'; import { toHex } from '../utils/num'; +import { intDAM } from '../utils/stark'; import { getExecuteCalldata } from '../utils/transaction'; import { getMessageHash } from '../utils/typedData'; import { SignerInterface } from './interface'; @@ -38,74 +45,93 @@ export class Signer implements SignerInterface { public async signTransaction( transactions: Call[], - transactionsDetail: InvocationsSignerDetails, - abis?: Abi[] + details: InvocationsSignerDetails ): Promise { - if (abis && abis.length !== transactions.length) { - throw new Error('ABI must be provided for each transaction or no transaction'); - } - // now use abi to display decoded data somewhere, but as this signer is headless, we can't do that - - const calldata = getExecuteCalldata(transactions, transactionsDetail.cairoVersion); + const compiledCalldata = getExecuteCalldata(transactions, details.cairoVersion); + let msgHash; - const msgHash = calculateTransactionHash( - transactionsDetail.walletAddress, - transactionsDetail.version, - calldata, - transactionsDetail.maxFee, - transactionsDetail.chainId, - transactionsDetail.nonce - ); + // TODO: How to do generic union discriminator for all like this + if (Object.values(ETransactionVersion2).includes(details.version as any)) { + const det = details as V2InvocationsSignerDetails; + msgHash = calculateInvokeTransactionHash({ + ...det, + senderAddress: det.walletAddress, + compiledCalldata, + version: det.version, + }); + } else if (Object.values(ETransactionVersion3).includes(details.version as any)) { + const det = details as V3InvocationsSignerDetails; + msgHash = calculateInvokeTransactionHash({ + ...det, + senderAddress: det.walletAddress, + compiledCalldata, + version: det.version, + nonceDataAvailabilityMode: intDAM(det.nonceDataAvailabilityMode), + feeDataAvailabilityMode: intDAM(det.feeDataAvailabilityMode), + }); + } else { + throw Error('unsupported signTransaction version'); + } - return starkCurve.sign(msgHash, this.pk); + return starkCurve.sign(msgHash as string, this.pk); } - public async signDeployAccountTransaction({ - classHash, - contractAddress, - constructorCalldata, - addressSalt, - maxFee, - version, - chainId, - nonce, - }: DeployAccountSignerDetails): Promise { - const msgHash = calculateDeployAccountTransactionHash( - contractAddress, - classHash, - CallData.compile(constructorCalldata), - addressSalt, - version, - maxFee, - chainId, - nonce - ); + public async signDeployAccountTransaction( + details: DeployAccountSignerDetails + ): Promise { + const compiledConstructorCalldata = CallData.compile(details.constructorCalldata); + /* const version = BigInt(details.version).toString(); */ + let msgHash; - return starkCurve.sign(msgHash, this.pk); + if (Object.values(ETransactionVersion2).includes(details.version as any)) { + const det = details as V2DeployAccountSignerDetails; + msgHash = calculateDeployAccountTransactionHash({ + ...det, + salt: det.addressSalt, + constructorCalldata: compiledConstructorCalldata, + version: det.version, + }); + } else if (Object.values(ETransactionVersion3).includes(details.version as any)) { + const det = details as V3DeployAccountSignerDetails; + msgHash = calculateDeployAccountTransactionHash({ + ...det, + salt: det.addressSalt, + compiledConstructorCalldata, + version: det.version, + nonceDataAvailabilityMode: intDAM(det.nonceDataAvailabilityMode), + feeDataAvailabilityMode: intDAM(det.feeDataAvailabilityMode), + }); + } else { + throw Error('unsupported signDeployAccountTransaction version'); + } + + return starkCurve.sign(msgHash as string, this.pk); } public async signDeclareTransaction( // contractClass: ContractClass, // Should be used once class hash is present in ContractClass - { - classHash, - senderAddress, - chainId, - maxFee, - version, - nonce, - compiledClassHash, - }: DeclareSignerDetails + details: DeclareSignerDetails ): Promise { - const msgHash = calculateDeclareTransactionHash( - classHash, - senderAddress, - version, - maxFee, - chainId, - nonce, - compiledClassHash - ); + let msgHash; - return starkCurve.sign(msgHash, this.pk); + if (Object.values(ETransactionVersion2).includes(details.version as any)) { + const det = details as V2DeclareSignerDetails; + msgHash = calculateDeclareTransactionHash({ + ...det, + version: det.version, + }); + } else if (Object.values(ETransactionVersion3).includes(details.version as any)) { + const det = details as V3DeclareSignerDetails; + msgHash = calculateDeclareTransactionHash({ + ...det, + version: det.version, + nonceDataAvailabilityMode: intDAM(det.nonceDataAvailabilityMode), + feeDataAvailabilityMode: intDAM(det.feeDataAvailabilityMode), + }); + } else { + throw Error('unsupported signDeclareTransaction version'); + } + + return starkCurve.sign(msgHash as string, this.pk); } } diff --git a/src/types/account.ts b/src/types/account.ts index 528d965ed..348999f74 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -1,4 +1,5 @@ -import { BigNumberish, BlockIdentifier } from './lib'; +import { ETransactionVersion } from './api/rpc'; +import { BigNumberish, BlockIdentifier, V3TransactionDetails } from './lib'; import { DeclareTransactionReceiptResponse, EstimateFeeResponse } from './provider'; export interface EstimateFee extends EstimateFeeResponse { @@ -7,11 +8,12 @@ export interface EstimateFee extends EstimateFeeResponse { export type EstimateFeeBulk = Array; +// TODO: This is too wide generic with optional params export type AccountInvocationsFactoryDetails = { - versions: bigint[]; + versions: Array<`${ETransactionVersion}`>; nonce?: BigNumberish; blockIdentifier?: BlockIdentifier; -}; +} & Partial; export interface EstimateFeeDetails { nonce?: BigNumberish; diff --git a/src/types/api/rpcspec_0_6/nonspec.ts b/src/types/api/rpcspec_0_6/nonspec.ts index 24c0b220a..0dfe2dfe3 100644 --- a/src/types/api/rpcspec_0_6/nonspec.ts +++ b/src/types/api/rpcspec_0_6/nonspec.ts @@ -130,12 +130,39 @@ export enum EBlockTag { PENDING = 'pending', } +// 'L1' | 'L2' export enum EDataAvailabilityMode { L1 = 'L1', L2 = 'L2', } +// 0 | 1 export enum EDAMode { L1, L2, } + +export enum ETransactionVersion { + V0 = '0x0', + V1 = '0x1', + V2 = '0x2', + V3 = '0x3', + F0 = '0x100000000000000000000000000000000', + F1 = '0x100000000000000000000000000000001', + F2 = '0x100000000000000000000000000000002', + F3 = '0x100000000000000000000000000000003', +} + +export enum ETransactionVersion2 { + V0 = '0x0', + V1 = '0x1', + V2 = '0x2', + F0 = '0x100000000000000000000000000000000', + F1 = '0x100000000000000000000000000000001', + F2 = '0x100000000000000000000000000000002', +} + +export enum ETransactionVersion3 { + V3 = '0x3', + F3 = '0x100000000000000000000000000000003', +} diff --git a/src/types/lib/index.ts b/src/types/lib/index.ts index 932e7056a..4651c4e75 100644 --- a/src/types/lib/index.ts +++ b/src/types/lib/index.ts @@ -1,6 +1,6 @@ import { StarknetChainId } from '../../constants'; import { weierstrass } from '../../utils/ec'; -import { V0_6 } from '../api/rpc'; +import { EDataAvailabilityMode, ResourceBounds } from '../api/rpc'; import { CairoEnum } from '../cairoEnum'; import { CompiledContract, CompiledSierraCasm, ContractClass } from './contract'; @@ -14,7 +14,7 @@ export type BigNumberish = string | number | bigint; * Compiled calldata ready to be sent * decimal-string array */ -export type Calldata = string[] & { readonly __compiled__?: boolean }; +export type Calldata = string[] & { readonly __compiled__?: true }; /** * Represents an integer in the range [0, 2^256) @@ -118,12 +118,12 @@ export type InvocationsDetails = { export type V3TransactionDetails = { nonce: BigNumberish; version: BigNumberish; - resourceBounds: V0_6.SPEC.RESOURCE_BOUNDS_MAPPING; + resourceBounds: ResourceBounds; tip: BigNumberish; paymasterData: BigNumberish[]; accountDeploymentData: BigNumberish[]; - nonceDataAvailabilityMode: V0_6.SPEC.DA_MODE; - feeDataAvailabilityMode: V0_6.SPEC.DA_MODE; + nonceDataAvailabilityMode: EDataAvailabilityMode; + feeDataAvailabilityMode: EDataAvailabilityMode; }; /** diff --git a/src/types/signer.ts b/src/types/signer.ts index c1474f47e..1d766ca40 100644 --- a/src/types/signer.ts +++ b/src/types/signer.ts @@ -1,29 +1,67 @@ import { StarknetChainId } from '../constants'; +import { ETransactionVersion, ETransactionVersion2, ETransactionVersion3 } from './api/rpc'; import { BigNumberish, CairoVersion, DeployAccountContractPayload, InvocationsDetails, + V3TransactionDetails, } from './lib'; -export interface InvocationsSignerDetails extends Required { +export type InvocationsSignerDetails = (V2InvocationsSignerDetails | V3InvocationsSignerDetails) & { + version: `${ETransactionVersion}`; +}; + +export type V2InvocationsSignerDetails = { walletAddress: string; + cairoVersion: CairoVersion; chainId: StarknetChainId; + nonce: BigNumberish; + maxFee: BigNumberish; + version: `${ETransactionVersion2}`; +}; + +export type V3InvocationsSignerDetails = V3TransactionDetails & { + walletAddress: string; cairoVersion: CairoVersion; -} + chainId: StarknetChainId; + version: `${ETransactionVersion3}`; +}; + +export type DeclareSignerDetails = (V3DeclareSignerDetails | V2DeclareSignerDetails) & { + version: `${ETransactionVersion}`; +}; -export interface DeclareSignerDetails { +export type V2DeclareSignerDetails = Required & { classHash: string; + compiledClassHash?: string; senderAddress: string; chainId: StarknetChainId; - maxFee: BigNumberish; - version: BigNumberish; - nonce: BigNumberish; - compiledClassHash?: string; -} + version: `${ETransactionVersion2}`; +}; + +export type V3DeclareSignerDetails = V3TransactionDetails & { + classHash: string; + compiledClassHash: string; + senderAddress: string; + chainId: StarknetChainId; + version: `${ETransactionVersion3}`; +}; -export type DeployAccountSignerDetails = Required & +export type DeployAccountSignerDetails = + | V2DeployAccountSignerDetails + | V3DeployAccountSignerDetails; + +export type V2DeployAccountSignerDetails = Required & Required & { contractAddress: BigNumberish; chainId: StarknetChainId; + version: `${ETransactionVersion2}`; + }; + +export type V3DeployAccountSignerDetails = Required & + V3TransactionDetails & { + contractAddress: BigNumberish; + chainId: StarknetChainId; + version: `${ETransactionVersion3}`; }; diff --git a/src/utils/hash.ts b/src/utils/hash/classHash.ts similarity index 60% rename from src/utils/hash.ts rename to src/utils/hash/classHash.ts index cef399b2b..9c953731c 100644 --- a/src/utils/hash.ts +++ b/src/utils/hash/classHash.ts @@ -1,18 +1,10 @@ -/* eslint-disable no-param-reassign */ -/* eslint-disable import/extensions */ +/** + * Class Hash + */ + import { poseidonHashMany } from '@scure/starknet'; -import { - API_VERSION, - BN_FEE_TRANSACTION_VERSION_1, - BN_FEE_TRANSACTION_VERSION_2, - BN_FEE_TRANSACTION_VERSION_3, - BN_TRANSACTION_VERSION_1, - BN_TRANSACTION_VERSION_2, - BN_TRANSACTION_VERSION_3, - StarknetChainId, - TransactionHashPrefix, -} from '../constants'; +import { API_VERSION } from '../../constants'; import { BigNumberish, Builtins, @@ -22,36 +14,15 @@ import { ContractEntryPointFields, LegacyCompiledContract, RawArgs, - RawCalldata, SierraContractEntryPointFields, -} from '../types'; -import { CallData } from './calldata'; -import { felt } from './calldata/cairo'; -import { starkCurve } from './ec'; -import { addHexPrefix, utf8ToArray } from './encode'; -import { parse, stringify } from './json'; -import { toBigInt, toHex } from './num'; -import { getSelectorFromName } from './selector'; -import { encodeShortString } from './shortString'; - -export * as poseidon from '@noble/curves/abstract/poseidon'; -export * from './selector'; // Preserve legacy export structure - -export const transactionVersion = BN_TRANSACTION_VERSION_1; -export const transactionVersion_2 = BN_TRANSACTION_VERSION_2; -export const transactionVersion_3 = BN_TRANSACTION_VERSION_3; -export const feeTransactionVersion = BN_FEE_TRANSACTION_VERSION_1; -export const feeTransactionVersion_2 = BN_FEE_TRANSACTION_VERSION_2; -export const feeTransactionVersion_3 = BN_FEE_TRANSACTION_VERSION_3; - -/** - * Return transaction versions based on version type, default version type is 'transaction' - */ -export function getVersionsByType(versionType?: 'fee' | 'transaction') { - return versionType === 'fee' - ? { v1: feeTransactionVersion, v2: feeTransactionVersion_2, v3: feeTransactionVersion_3 } - : { v1: transactionVersion, v2: transactionVersion_2, v3: transactionVersion_3 }; -} +} from '../../types'; +import { CallData } from '../calldata'; +import { felt } from '../calldata/cairo'; +import { starkCurve } from '../ec'; +import { addHexPrefix, utf8ToArray } from '../encode'; +import { parse, stringify } from '../json'; +import { toBigInt, toHex } from '../num'; +import { encodeShortString } from '../shortString'; /** * Compute pedersen hash from data @@ -63,137 +34,6 @@ export function computeHashOnElements(data: BigNumberish[]): string { .toString(); } -/** - * Calculate transaction pedersen hash for common properties - * - * Following implementation is based on this python [implementation #](https://github.com/starkware-libs/cairo-lang/blob/b614d1867c64f3fb2cf4a4879348cfcf87c3a5a7/src/starkware/starknet/core/os/transaction_hash/transaction_hash.py) - * @returns format: hex-string - */ -export function calculateTransactionHashCommon( - txHashPrefix: TransactionHashPrefix, - version: BigNumberish, - contractAddress: BigNumberish, - entryPointSelector: BigNumberish, - calldata: RawCalldata, - maxFee: BigNumberish, - chainId: StarknetChainId, - additionalData: BigNumberish[] = [] -): string { - const calldataHash = computeHashOnElements(calldata); - const dataToHash = [ - txHashPrefix, - version, - contractAddress, - entryPointSelector, - calldataHash, - maxFee, - chainId, - ...additionalData, - ]; - return computeHashOnElements(dataToHash); -} - -/** - * Calculate deploy transaction hash - * @returns format: hex-string - */ -export function calculateDeployTransactionHash( - contractAddress: BigNumberish, - constructorCalldata: RawCalldata, - version: BigNumberish, - chainId: StarknetChainId, - constructorName: string = 'constructor' -): string { - return calculateTransactionHashCommon( - TransactionHashPrefix.DEPLOY, - version, - contractAddress, - getSelectorFromName(constructorName), - constructorCalldata, - 0, - chainId - ); -} - -/** - * Calculate declare transaction hash - * @param classHash hex-string - * @param compiledClassHash hex-string - * @returns format: hex-string - */ -export function calculateDeclareTransactionHash( - classHash: string, - senderAddress: BigNumberish, - version: BigNumberish, - maxFee: BigNumberish, - chainId: StarknetChainId, - nonce: BigNumberish, - compiledClassHash?: string -): string { - return calculateTransactionHashCommon( - TransactionHashPrefix.DECLARE, - version, - senderAddress, - 0, - [classHash], - maxFee, - chainId, - [nonce, ...(compiledClassHash ? [compiledClassHash] : [])] - ); -} - -/** - * Calculate deploy_account transaction hash - * @returns format: hex-string - */ -export function calculateDeployAccountTransactionHash( - contractAddress: BigNumberish, - classHash: BigNumberish, - constructorCalldata: RawCalldata, - salt: BigNumberish, - version: BigNumberish, - maxFee: BigNumberish, - chainId: StarknetChainId, - nonce: BigNumberish -) { - const calldata = [classHash, salt, ...constructorCalldata]; - - return calculateTransactionHashCommon( - TransactionHashPrefix.DEPLOY_ACCOUNT, - version, - contractAddress, - 0, - calldata, - maxFee, - chainId, - [nonce] - ); -} - -/** - * Calculate invoke transaction hash - * @returns format: hex-string - */ -export function calculateTransactionHash( - contractAddress: BigNumberish, - version: BigNumberish, - calldata: RawCalldata, - maxFee: BigNumberish, - chainId: StarknetChainId, - nonce: BigNumberish -): string { - return calculateTransactionHashCommon( - TransactionHashPrefix.INVOKE, - version, - contractAddress, - 0, - calldata, - maxFee, - chainId, - [nonce] - ); -} - /** * Calculate contract address from class hash * @returns format: hex-string @@ -306,7 +146,7 @@ export function computeLegacyContractClassHash(contract: LegacyCompiledContract ]); } -// Cairo 1 code +// Cairo 1 Contract Hashes function hashBuiltins(builtins: Builtins) { return poseidonHashMany( diff --git a/src/utils/hash/index.ts b/src/utils/hash/index.ts new file mode 100644 index 000000000..528ce6361 --- /dev/null +++ b/src/utils/hash/index.ts @@ -0,0 +1,8 @@ +/** + * Hashes Exports + */ +export * as poseidon from '@noble/curves/abstract/poseidon'; +export * from '../selector'; // Preserve legacy export structure + +export * from './transactionHash'; +export * from './classHash'; diff --git a/src/utils/hash/transactionHash/index.ts b/src/utils/hash/transactionHash/index.ts new file mode 100644 index 000000000..0cf2d4f2a --- /dev/null +++ b/src/utils/hash/transactionHash/index.ts @@ -0,0 +1,219 @@ +/** + * Transaction Hash based on Transaction Version + */ + +import { StarknetChainId } from '../../../constants'; +import { BigNumberish, Calldata } from '../../../types'; +import { + EDAMode, + ETransactionVersion, + ETransactionVersion2, + ETransactionVersion3, + ResourceBounds, +} from '../../../types/api/rpc'; +import { + calculateDeclareTransactionHash as v2calculateDeclareTransactionHash, + calculateDeployAccountTransactionHash as v2calculateDeployAccountTransactionHash, + calculateTransactionHash as v2calculateInvokeTransactionHash, +} from './v2'; +import { + calculateDeclareTransactionHash as v3calculateDeclareTransactionHash, + calculateDeployAccountTransactionHash as v3calculateDeployAccountTransactionHash, + calculateInvokeTransactionHash as v3calculateInvokeTransactionHash, +} from './v3'; + +/* + * INVOKE TX HASH + */ + +function isV3InvokeTx(args: CalcInvokeTxHashArgs): args is CalcV3InvokeTxHashArgs { + return [ETransactionVersion.V3, ETransactionVersion.F3].includes( + args.version as ETransactionVersion + ); +} + +type CalcV2InvokeTxHashArgs = { + senderAddress: BigNumberish; + version: `${ETransactionVersion2}`; + compiledCalldata: Calldata; + maxFee: BigNumberish; + chainId: StarknetChainId; + nonce: BigNumberish; +}; + +type CalcV3InvokeTxHashArgs = { + senderAddress: BigNumberish; + version: `${ETransactionVersion3}`; + compiledCalldata: Calldata; + chainId: StarknetChainId; + nonce: BigNumberish; + accountDeploymentData: BigNumberish[]; + nonceDataAvailabilityMode: EDAMode; + feeDataAvailabilityMode: EDAMode; + resourceBounds: ResourceBounds; + tip: BigNumberish; + paymasterData: BigNumberish[]; +}; + +type CalcInvokeTxHashArgs = CalcV2InvokeTxHashArgs | CalcV3InvokeTxHashArgs; + +export function calculateInvokeTransactionHash(args: CalcInvokeTxHashArgs) { + if (isV3InvokeTx(args)) { + return v3calculateInvokeTransactionHash( + args.senderAddress, + args.version, + args.compiledCalldata, + args.chainId, + args.nonce, + args.accountDeploymentData, + args.nonceDataAvailabilityMode, + args.feeDataAvailabilityMode, + args.resourceBounds, + args.tip, + args.paymasterData + ); + } + return v2calculateInvokeTransactionHash( + args.senderAddress, + args.version, + args.compiledCalldata, + args.maxFee, + args.chainId, + args.nonce + ); +} + +/* + * DECLARE TX HASH + */ +function isV3DeclareTx(args: CalcDeclareTxHashArgs): args is CalcV3DeclareTxHashArgs { + return [ETransactionVersion.V3, ETransactionVersion.F3].includes( + args.version as ETransactionVersion + ); +} + +type CalcV2DeclareTxHashArgs = { + classHash: string; + senderAddress: BigNumberish; + version: `${ETransactionVersion2}`; + maxFee: BigNumberish; + chainId: StarknetChainId; + nonce: BigNumberish; + compiledClassHash?: string; +}; + +type CalcV3DeclareTxHashArgs = { + classHash: string; + compiledClassHash: string; + senderAddress: BigNumberish; + version: `${ETransactionVersion3}`; + chainId: StarknetChainId; + nonce: BigNumberish; + accountDeploymentData: BigNumberish[]; + nonceDataAvailabilityMode: EDAMode; + feeDataAvailabilityMode: EDAMode; + resourceBounds: ResourceBounds; + tip: BigNumberish; + paymasterData: BigNumberish[]; +}; + +type CalcDeclareTxHashArgs = CalcV2DeclareTxHashArgs | CalcV3DeclareTxHashArgs; + +export function calculateDeclareTransactionHash(args: CalcDeclareTxHashArgs) { + if (isV3DeclareTx(args)) { + return v3calculateDeclareTransactionHash( + args.classHash, + args.compiledClassHash, + args.senderAddress, + args.version, + args.chainId, + args.nonce, + args.accountDeploymentData, + args.nonceDataAvailabilityMode, + args.feeDataAvailabilityMode, + args.resourceBounds, + args.tip, + args.paymasterData + ); + } + + return v2calculateDeclareTransactionHash( + args.classHash, + args.senderAddress, + args.version, + args.maxFee, + args.chainId, + args.nonce, + args.compiledClassHash + ); +} + +/* + * DEPLOY ACCOUNT TX HASH + */ + +function isV3DeployAccountTx( + args: CalcDeployAccountTxHashArgs +): args is CalcV3DeployAccountTxHashArgs { + return [ETransactionVersion.V3, ETransactionVersion.F3].includes( + args.version as ETransactionVersion + ); +} + +type CalcV2DeployAccountTxHashArgs = { + contractAddress: BigNumberish; + classHash: BigNumberish; + constructorCalldata: Calldata; + salt: BigNumberish; + version: `${ETransactionVersion2}`; + maxFee: BigNumberish; + chainId: StarknetChainId; + nonce: BigNumberish; +}; + +type CalcV3DeployAccountTxHashArgs = { + contractAddress: BigNumberish; + classHash: BigNumberish; + compiledConstructorCalldata: Calldata; + salt: BigNumberish; + version: `${ETransactionVersion3}`; + chainId: StarknetChainId; + nonce: BigNumberish; + nonceDataAvailabilityMode: EDAMode; + feeDataAvailabilityMode: EDAMode; + resourceBounds: ResourceBounds; + tip: BigNumberish; + paymasterData: BigNumberish[]; +}; + +type CalcDeployAccountTxHashArgs = CalcV2DeployAccountTxHashArgs | CalcV3DeployAccountTxHashArgs; + +export function calculateDeployAccountTransactionHash(args: CalcDeployAccountTxHashArgs) { + if (isV3DeployAccountTx(args)) { + return v3calculateDeployAccountTransactionHash( + args.contractAddress, + args.classHash, + args.compiledConstructorCalldata, + args.salt, + args.version, + args.chainId, + args.nonce, + args.nonceDataAvailabilityMode, + args.feeDataAvailabilityMode, + args.resourceBounds, + args.tip, + args.paymasterData + ); + } + + return v2calculateDeployAccountTransactionHash( + args.contractAddress, + args.classHash, + args.constructorCalldata, + args.salt, + args.version, + args.maxFee, + args.chainId, + args.nonce + ); +} diff --git a/src/utils/hash/transactionHash/v2.ts b/src/utils/hash/transactionHash/v2.ts new file mode 100644 index 000000000..253824a6b --- /dev/null +++ b/src/utils/hash/transactionHash/v2.ts @@ -0,0 +1,129 @@ +/** + * Calculate Hashes for v0 - v2 transactions + */ + +/* eslint-disable no-param-reassign */ +/* eslint-disable import/extensions */ +import { StarknetChainId, TransactionHashPrefix } from '../../../constants'; +import { BigNumberish, RawCalldata } from '../../../types'; +import { starkCurve } from '../../ec'; +import { toBigInt } from '../../num'; + +/** + * Compute pedersen hash from data + * @returns format: hex-string - pedersen hash + */ +export function computeHashOnElements(data: BigNumberish[]): string { + return [...data, data.length] + .reduce((x: BigNumberish, y: BigNumberish) => starkCurve.pedersen(toBigInt(x), toBigInt(y)), 0) + .toString(); +} + +/** + * Calculate transaction pedersen hash for common properties + * + * Following implementation is based on this python [implementation #](https://github.com/starkware-libs/cairo-lang/blob/b614d1867c64f3fb2cf4a4879348cfcf87c3a5a7/src/starkware/starknet/core/os/transaction_hash/transaction_hash.py) + * @returns format: hex-string + */ +export function calculateTransactionHashCommon( + txHashPrefix: TransactionHashPrefix, + version: BigNumberish, + contractAddress: BigNumberish, + entryPointSelector: BigNumberish, + calldata: RawCalldata, + maxFee: BigNumberish, + chainId: StarknetChainId, + additionalData: BigNumberish[] = [] +): string { + const calldataHash = computeHashOnElements(calldata); + const dataToHash = [ + txHashPrefix, + version, + contractAddress, + entryPointSelector, + calldataHash, + maxFee, + chainId, + ...additionalData, + ]; + return computeHashOnElements(dataToHash); +} + +/** + * Calculate declare transaction hash + * @param classHash hex-string + * @param compiledClassHash hex-string + * @returns format: hex-string + */ +export function calculateDeclareTransactionHash( + classHash: string, + senderAddress: BigNumberish, + version: BigNumberish, + maxFee: BigNumberish, + chainId: StarknetChainId, + nonce: BigNumberish, + compiledClassHash?: string +): string { + return calculateTransactionHashCommon( + TransactionHashPrefix.DECLARE, + version, + senderAddress, + 0, + [classHash], + maxFee, + chainId, + [nonce, ...(compiledClassHash ? [compiledClassHash] : [])] + ); +} + +/** + * Calculate deploy_account transaction hash + * @returns format: hex-string + */ +export function calculateDeployAccountTransactionHash( + contractAddress: BigNumberish, + classHash: BigNumberish, + constructorCalldata: RawCalldata, + salt: BigNumberish, + version: BigNumberish, + maxFee: BigNumberish, + chainId: StarknetChainId, + nonce: BigNumberish +) { + const calldata = [classHash, salt, ...constructorCalldata]; + + return calculateTransactionHashCommon( + TransactionHashPrefix.DEPLOY_ACCOUNT, + version, + contractAddress, + 0, + calldata, + maxFee, + chainId, + [nonce] + ); +} + +/** + * Calculate invoke transaction hash + * @returns format: hex-string + */ +export function calculateTransactionHash( + contractAddress: BigNumberish, + version: BigNumberish, + calldata: RawCalldata, + maxFee: BigNumberish, + chainId: StarknetChainId, + nonce: BigNumberish +): string { + return calculateTransactionHashCommon( + TransactionHashPrefix.INVOKE, + version, + contractAddress, + 0, + calldata, + maxFee, + chainId, + [nonce] + ); +} diff --git a/src/utils/v3hash.ts b/src/utils/hash/transactionHash/v3.ts similarity index 92% rename from src/utils/v3hash.ts rename to src/utils/hash/transactionHash/v3.ts index 8709aa3a7..272ccf0f1 100644 --- a/src/utils/v3hash.ts +++ b/src/utils/hash/transactionHash/v3.ts @@ -1,10 +1,14 @@ +/** + * Calculate Hashes for v3 transactions + */ + import { poseidonHashMany } from '@scure/starknet'; -import { StarknetChainId, TransactionHashPrefix } from '../constants'; -import { BigNumberish, Calldata } from '../types'; -import { EDAMode, ResourceBounds } from '../types/api/rpc'; -import { toHex } from './num'; -import { encodeShortString } from './shortString'; +import { StarknetChainId, TransactionHashPrefix } from '../../../constants'; +import { BigNumberish, Calldata } from '../../../types'; +import { EDAMode, ResourceBounds } from '../../../types/api/rpc'; +import { toHex } from '../../num'; +import { encodeShortString } from '../../shortString'; const AToBI = (array: BigNumberish[]) => array.map((it: BigNumberish) => BigInt(it)); @@ -144,7 +148,7 @@ export function calculateInvokeTransactionHash( feeDataAvailabilityMode: EDAMode, resourceBounds: ResourceBounds, tip: BigNumberish, - paymasterData: [] + paymasterData: BigNumberish[] ): string { return calculateTransactionHashCommon( TransactionHashPrefix.INVOKE, diff --git a/src/utils/num.ts b/src/utils/num.ts index 9b41f7c5c..d3e9f9c45 100644 --- a/src/utils/num.ts +++ b/src/utils/num.ts @@ -170,3 +170,14 @@ export function hexToBytes(value: string): Uint8Array { } return hexToBytesNoble(adaptedValue); } + +/** + * + * @param number value to be increased + * @param percent integer as percent ex. 50 for 50% + * @returns increased value + */ +export function addPercent(number: BigNumberish, percent: number) { + const bigIntNum = BigInt(number); + return bigIntNum + (bigIntNum * BigInt(percent)) / 100n; +} diff --git a/src/utils/stark.ts b/src/utils/stark.ts index 2d0e454a2..988688f5a 100644 --- a/src/utils/stark.ts +++ b/src/utils/stark.ts @@ -1,13 +1,21 @@ import { getStarkKey, utils } from '@scure/starknet'; import { gzip, ungzip } from 'pako'; -import { ArraySignatureType, BigNumberish, CompressedProgram, Program, Signature } from '../types'; +import { + ArraySignatureType, + BigNumberish, + CompressedProgram, + EstimateFeeResponse, + Program, + Signature, +} from '../types'; +import { EDAMode, EDataAvailabilityMode, ResourceBounds } from '../types/api/rpc'; import { addHexPrefix, arrayBufferToString, atobUniversal, btoaUniversal } from './encode'; import { parse, stringify } from './json'; import { + addPercent, bigNumberishArrayToDecimalStringArray, bigNumberishArrayToHexadecimalStringArray, - toBigInt, toHex, } from './num'; @@ -86,7 +94,34 @@ export function signatureToHexArray(sig?: Signature): ArraySignatureType { * Convert estimated fee to max fee with overhead */ export function estimatedFeeToMaxFee(estimatedFee: BigNumberish, overhead: number = 0.5): bigint { - // BN can only handle Integers, so we need to do all calculations with integers - const overHeadPercent = Math.round((1 + overhead) * 100); - return (toBigInt(estimatedFee) * toBigInt(overHeadPercent)) / 100n; + return addPercent(estimatedFee, overhead * 100); +} + +export function estimateFeeToBounds( + estimate: EstimateFeeResponse | 0n, + amountOverhead: number = 10, + priceOverhead = 50 +): ResourceBounds { + if (typeof estimate === 'bigint') { + return { + l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + l1_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + }; + } + + if (typeof estimate.gas_consumed === 'undefined' || typeof estimate.gas_price === 'undefined') { + throw Error('estimateFeeToBounds: estimate is undefined'); + } + const maxUnits = toHex(addPercent(estimate.gas_consumed, amountOverhead)); + const maxUnitPrice = toHex(addPercent(estimate.gas_price, priceOverhead)); + return { + l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, + l1_gas: { max_amount: maxUnits, max_price_per_unit: maxUnitPrice }, + }; +} + +export function intDAM(dam: EDataAvailabilityMode) { + if (dam === EDataAvailabilityMode.L1) return EDAMode.L1; + if (dam === EDataAvailabilityMode.L2) return EDAMode.L2; + throw Error('EDAM conversion'); } diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index b651fab72..a09df0507 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -1,3 +1,11 @@ +import { + BN_FEE_TRANSACTION_VERSION_1, + BN_FEE_TRANSACTION_VERSION_2, + BN_FEE_TRANSACTION_VERSION_3, + BN_TRANSACTION_VERSION_1, + BN_TRANSACTION_VERSION_2, + BN_TRANSACTION_VERSION_3, +} from '../constants'; import { BigNumberish, CairoVersion, @@ -92,3 +100,16 @@ export const getExecuteCalldata = (calls: Call[], cairoVersion: CairoVersion = ' } return fromCallsToExecuteCalldata(calls); }; + +/** + * Return transaction versions based on version type, default version type is 'transaction' + */ +export function getVersionsByType(versionType?: 'fee' | 'transaction') { + return versionType === 'fee' + ? { + v1: BN_FEE_TRANSACTION_VERSION_1, + v2: BN_FEE_TRANSACTION_VERSION_2, + v3: BN_FEE_TRANSACTION_VERSION_3, + } + : { v1: BN_TRANSACTION_VERSION_1, v2: BN_TRANSACTION_VERSION_2, v3: BN_TRANSACTION_VERSION_3 }; +}