diff --git a/.changeset/brown-zebras-remember.md b/.changeset/brown-zebras-remember.md new file mode 100644 index 00000000000..c9faaaa1328 --- /dev/null +++ b/.changeset/brown-zebras-remember.md @@ -0,0 +1,8 @@ +--- +"@fuel-ts/hasher": minor +"@fuel-ts/program": minor +"@fuel-ts/providers": minor +"@fuel-ts/wallet": minor +--- + +add method to hash tx on TransactionRequest classes diff --git a/apps/docs-snippets/src/guide/cookbook/transferring-assets.test.ts b/apps/docs-snippets/src/guide/cookbook/transferring-assets.test.ts new file mode 100644 index 00000000000..59f3e2c0429 --- /dev/null +++ b/apps/docs-snippets/src/guide/cookbook/transferring-assets.test.ts @@ -0,0 +1,163 @@ +import type { Contract, Provider, TxParams, WalletUnlocked } from 'fuels'; +import { Address, BN, ContractFactory, BaseAssetId, Wallet } from 'fuels'; + +import { + DocSnippetProjectsEnum, + getDocsSnippetsForcProject, +} from '../../../test/fixtures/forc-projects'; +import { getTestWallet } from '../../utils'; + +describe(__filename, () => { + let sender: WalletUnlocked; + let deployedContract: Contract; + let provider: Provider; + + beforeAll(async () => { + sender = await getTestWallet(); + + const { abiContents, binHexlified } = getDocsSnippetsForcProject( + DocSnippetProjectsEnum.COUNTER + ); + provider = sender.provider; + const factory = new ContractFactory(binHexlified, abiContents, sender); + const { minGasPrice } = sender.provider.getGasConfig(); + deployedContract = await factory.deployContract({ gasPrice: minGasPrice }); + }); + + it('should successfully transfer asset to another account', async () => { + // #region transferring-assets-1 + // #context import { Wallet, BN, BaseAssetId } from 'fuels'; + + // #context const sender = Wallet.fromPrivateKey('...'); + const destination = Wallet.generate({ + provider: sender.provider, + }); + const amountToTransfer = 500; + const assetId = BaseAssetId; + + const { minGasPrice } = provider.getGasConfig(); + + const txParams: TxParams = { + gasPrice: minGasPrice, + gasLimit: 1_000, + }; + + const response = await sender.transfer( + destination.address, + amountToTransfer, + assetId, + txParams + ); + + await response.wait(); + + // Retrieve balances + const receiverBalance = await destination.getBalance(assetId); + + // Validate new balance + expect(new BN(receiverBalance).toNumber()).toEqual(amountToTransfer); + // #endregion transferring-assets-1 + }); + + it('should successfully prepare transfer to another account', async () => { + const destination = Wallet.generate({ + provider: sender.provider, + }); + + const amountToTransfer = 200; + const assetId = BaseAssetId; + + const { minGasPrice } = provider.getGasConfig(); + + const txParams: TxParams = { + gasPrice: minGasPrice, + gasLimit: 1_000, + }; + + // #region transferring-assets-2 + const transactionRequest = await sender.createTransfer( + destination.address, + amountToTransfer, + assetId, + txParams + ); + + const chainId = provider.getChainId(); + + const transactionId = transactionRequest.getTransactionId(chainId); + + const response = await sender.sendTransaction(transactionRequest); + + const { id } = await response.wait(); + + // The transaction id should is the same as the one returned by the transaction request + expect(id).toEqual(transactionId); + // #endregion transferring-assets-2 + }); + + it('should validate that modifying the transaction request will result in another TX ID', async () => { + const destination = Wallet.generate({ + provider: sender.provider, + }); + + const amountToTransfer = 200; + const assetId = BaseAssetId; + + const { minGasPrice } = provider.getGasConfig(); + + const txParams: TxParams = { + gasPrice: minGasPrice, + gasLimit: 1_000, + }; + + // #region transferring-assets-3 + const transactionRequest = await sender.createTransfer( + destination.address, + amountToTransfer, + assetId, + txParams + ); + + const chainId = provider.getChainId(); + + const transactionId = transactionRequest.getTransactionId(chainId); + + transactionRequest.maturity = 1; + + const response = await sender.sendTransaction(transactionRequest); + + const { id } = await response.wait(); + + expect(id).not.toEqual(transactionId); + // #endregion transferring-assets-3 + }); + + it('should successfully prepare transfer transaction request', async () => { + const contractId = Address.fromAddressOrString(deployedContract.id); + // #region transferring-assets-4 + // #context import { Wallet, BN, BaseAssetId } from 'fuels'; + + // #context const senderWallet = Wallet.fromPrivateKey('...'); + + const amountToTransfer = 400; + const assetId = BaseAssetId; + // #context const contractId = Address.fromAddressOrString('0x123...'); + + const contractBalance = await deployedContract.getBalance(assetId); + + const { minGasPrice } = provider.getGasConfig(); + + const txParams: TxParams = { + gasPrice: minGasPrice, + gasLimit: 1_000, + }; + + const tx = await sender.transferToContract(contractId, amountToTransfer, assetId, txParams); + expect(new BN(contractBalance).toNumber()).toBe(0); + + await tx.waitForResult(); + + expect(new BN(await deployedContract.getBalance(assetId)).toNumber()).toBe(amountToTransfer); + // #endregion transferring-assets-4 + }); +}); diff --git a/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts b/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts index 4ad9e9b0ae3..6cd38a81e05 100644 --- a/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts +++ b/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts @@ -1,19 +1,16 @@ import { safeExec } from '@fuel-ts/errors/test-utils'; +import type { Provider } from 'fuels'; +import { WalletUnlocked, Predicate, BN, getRandomB256, BaseAssetId } from 'fuels'; + import { - WalletUnlocked, - FUEL_NETWORK_URL, - Provider, - Predicate, - BN, - getRandomB256, - BaseAssetId, -} from 'fuels'; - -import { DocSnippetProjectsEnum, getDocsSnippetsForcProject } from '../../../test/fixtures/forc-projects'; + DocSnippetProjectsEnum, + getDocsSnippetsForcProject, +} from '../../../test/fixtures/forc-projects'; import { getTestWallet } from '../../utils'; describe(__filename, () => { let walletWithFunds: WalletUnlocked; + let provider: Provider; let gasPrice: BN; const { abiContents: abi, binHexlified: bin } = getDocsSnippetsForcProject( DocSnippetProjectsEnum.SIMPLE_PREDICATE @@ -21,21 +18,21 @@ describe(__filename, () => { beforeAll(async () => { walletWithFunds = await getTestWallet(); - ({ minGasPrice: gasPrice } = walletWithFunds.provider.getGasConfig()); + provider = walletWithFunds.provider; + ({ minGasPrice: gasPrice } = provider.getGasConfig()); }); it('should successfully use predicate to spend assets', async () => { // #region send-and-spend-funds-from-predicates-2 - const provider = await Provider.create(FUEL_NETWORK_URL); const predicate = new Predicate(bin, provider, abi); // #endregion send-and-spend-funds-from-predicates-2 // #region send-and-spend-funds-from-predicates-3 - const amountToPredicate = 300_000; + const amountToPredicate = 10_000; const tx = await walletWithFunds.transfer(predicate.address, amountToPredicate, BaseAssetId, { gasPrice, - gasLimit: 10_000, + gasLimit: 1_000, }); await tx.waitForResult(); @@ -58,11 +55,11 @@ describe(__filename, () => { const tx2 = await predicate.transfer( receiverWallet.address, - amountToPredicate - 150_000, + amountToPredicate - 1000, BaseAssetId, { gasPrice, - gasLimit: 10_000, + gasLimit: 1_000, } ); @@ -71,14 +68,13 @@ describe(__filename, () => { }); it('should fail when trying to spend predicates entire amount', async () => { - const provider = await Provider.create(FUEL_NETWORK_URL); const predicate = new Predicate(bin, provider, abi); const amountToPredicate = 100; const tx = await walletWithFunds.transfer(predicate.address, amountToPredicate, BaseAssetId, { gasPrice, - gasLimit: 10_000, + gasLimit: 1_000, }); await tx.waitForResult(); @@ -94,7 +90,7 @@ describe(__filename, () => { const { error } = await safeExec(() => predicate.transfer(receiverWallet.address, predicateBalance, BaseAssetId, { gasPrice, - gasLimit: 10_000, + gasLimit: 1_000, }) ); @@ -106,17 +102,16 @@ describe(__filename, () => { }); it('should fail when set wrong input data for predicate', async () => { - const provider = await Provider.create(FUEL_NETWORK_URL); const predicateOwner = WalletUnlocked.generate({ provider, }); const predicate = new Predicate(bin, predicateOwner.provider, abi); - const amountToPredicate = 200_000; + const amountToPredicate = 10_000; const tx = await walletWithFunds.transfer(predicate.address, amountToPredicate, BaseAssetId, { gasPrice, - gasLimit: 10_000, + gasLimit: 1_000, }); await tx.waitForResult(); @@ -130,7 +125,7 @@ describe(__filename, () => { const { error } = await safeExec(() => predicate.transfer(receiverWallet.address, amountToPredicate, BaseAssetId, { gasPrice, - gasLimit: 10_000, + gasLimit: 1_000, }) ); @@ -140,4 +135,48 @@ describe(__filename, () => { expect((error).message).toMatch(errorMsg); }); + + it('should ensure predicate createTransfer works as expected', async () => { + const predicate = new Predicate(bin, provider, abi); + + const amountToPredicate = 10_000; + + const tx = await walletWithFunds.transfer(predicate.address, amountToPredicate, BaseAssetId, { + gasPrice, + gasLimit: 1_000, + }); + + await tx.waitForResult(); + + const inputAddress = '0xfc05c23a8f7f66222377170ddcbfea9c543dff0dd2d2ba4d0478a4521423a9d4'; + + predicate.setData(inputAddress); + + const receiverWallet = WalletUnlocked.generate({ + provider, + }); + + // #region send-and-spend-funds-from-predicates-8 + const transactionRequest = await predicate.createTransfer( + receiverWallet.address, + amountToPredicate, + BaseAssetId, + { + gasPrice, + gasLimit: 1_000, + } + ); + + const chainId = provider.getChainId(); + + const txId = transactionRequest.getTransactionId(chainId); + + const res = await predicate.sendTransaction(transactionRequest); + + await res.waitForResult(); + // #endregion send-and-spend-funds-from-predicates-8 + const txIdFromExecutedTx = res.id; + + expect(txId).toEqual(txIdFromExecutedTx); + }); }); diff --git a/apps/docs-snippets/src/guide/wallets/transferring-assets.test.ts b/apps/docs-snippets/src/guide/wallets/transferring-assets.test.ts deleted file mode 100644 index f2e01b203de..00000000000 --- a/apps/docs-snippets/src/guide/wallets/transferring-assets.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { Contract, Provider, TxParams, WalletUnlocked } from 'fuels'; -import { Address, BN, ContractFactory, BaseAssetId, Wallet } from 'fuels'; - -import { - DocSnippetProjectsEnum, - getDocsSnippetsForcProject, -} from '../../../test/fixtures/forc-projects'; -import { getTestWallet } from '../../utils'; - -describe(__filename, () => { - let senderWallet: WalletUnlocked; - let deployedContract: Contract; - let provider: Provider; - - beforeAll(async () => { - senderWallet = await getTestWallet(); - - const { abiContents, binHexlified } = getDocsSnippetsForcProject( - DocSnippetProjectsEnum.COUNTER - ); - provider = senderWallet.provider; - const factory = new ContractFactory(binHexlified, abiContents, senderWallet); - const { minGasPrice } = senderWallet.provider.getGasConfig(); - deployedContract = await factory.deployContract({ gasPrice: minGasPrice }); - }); - - it('should successfully transfer asset to another wallet', async () => { - // #region transferring-assets-1 - // #context import { Wallet, BN, BaseAssetId } from 'fuels'; - - // #context const senderWallet = Wallet.fromPrivateKey('...'); - const destinationWallet = Wallet.generate({ - provider: senderWallet.provider, - }); - const amountToTransfer = 500; - const assetId = BaseAssetId; - - const { minGasPrice } = provider.getGasConfig(); - - const txParams: TxParams = { - gasPrice: minGasPrice, - gasLimit: 10_000, - }; - - const response = await senderWallet.transfer( - destinationWallet.address, - amountToTransfer, - assetId, - txParams - ); - - await response.wait(); - - // Retrieve balances - const receiverBalance = await destinationWallet.getBalance(assetId); - - // Validate new balance - expect(new BN(receiverBalance).toNumber()).toEqual(amountToTransfer); - // #endregion transferring-assets-1 - }); - - it('should successfully transfer asset to a deployed contract', async () => { - const contractId = Address.fromAddressOrString(deployedContract.id); - // #region transferring-assets-2 - // #context import { Wallet, BN, BaseAssetId } from 'fuels'; - - // #context const senderWallet = Wallet.fromPrivateKey('...'); - - const amountToTransfer = 400; - const assetId = BaseAssetId; - // #context const contractId = Address.fromAddressOrString('0x123...'); - - const contractBalance = await deployedContract.getBalance(assetId); - - const { minGasPrice } = provider.getGasConfig(); - - const txParams: TxParams = { - gasPrice: minGasPrice, - gasLimit: 10_000, - }; - - const tx = await senderWallet.transferToContract( - contractId, - amountToTransfer, - assetId, - txParams - ); - expect(new BN(contractBalance).toNumber()).toBe(0); - - await tx.waitForResult(); - - expect(new BN(await deployedContract.getBalance(assetId)).toNumber()).toBe(amountToTransfer); - // #endregion transferring-assets-2 - }); -}); diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts index b7406b190f3..5d7a77fd4f6 100644 --- a/apps/docs/.vitepress/config.ts +++ b/apps/docs/.vitepress/config.ts @@ -201,10 +201,6 @@ export default defineConfig({ text: 'Test Wallets', link: '/guide/wallets/test-wallets', }, - { - text: 'Transferring Assets', - link: '/guide/wallets/transferring-assets', - }, ], }, { @@ -328,6 +324,10 @@ export default defineConfig({ link: '/guide/cookbook/', collapsed: true, items: [ + { + text: 'Transferring Assets', + link: '/guide/cookbook/transferring-assets', + }, { text: 'Custom Transactions', link: '/guide/cookbook/custom-transactions', diff --git a/apps/docs/src/guide/cookbook/transferring-assets.md b/apps/docs/src/guide/cookbook/transferring-assets.md new file mode 100644 index 00000000000..fb7a09027d8 --- /dev/null +++ b/apps/docs/src/guide/cookbook/transferring-assets.md @@ -0,0 +1,47 @@ +# Transferring Assets + +This documentation provides a guide on how to transfer assets between wallets and to contracts using the SDK. + +## Transferring Assets Between Accounts + +The `account.transfer` method is used to initiate a transaction request that transfers an asset from one wallet to another. This method requires three parameters: + +1. The receiver's wallet address. +2. The amount of the asset to be transferred. +3. The ID of the asset to be transferred (Optional - Defaults to the base asset id). + +Upon execution, this function returns a promise that resolves to a transaction response. To wait for the transaction to be processed, call `response.wait()`. + +### Example + +Here is an illustration on how to use the `account.transfer` function: + +<<< @/../../docs-snippets/src/guide/cookbook/transferring-assets.test.ts#transferring-assets-1{ts:line-numbers} + +In the previous example, we used the `transfer` method, which essentially creates a `ScriptTransactionRequest`, populates its data with the provided transfer information, and submits the transaction request to the node. + +However, there may be times when you need the Transaction ID before actually submitting it to the node. To achieve this, you can simply call the `createTransfer` method instead. + +This method also creates a `ScriptTransactionRequest` and populates it with the provided data, but instead of submitting it to the node, it returns the request. + +<<< @/../../docs-snippets/src/guide/cookbook/transferring-assets.test.ts#transferring-assets-2{ts:line-numbers} + +It's important to be aware that once you have this transaction request, any modifications made to it will result in a new and distinct transaction. Consequently, this will lead to a different transaction ID. Therefore, make sure to not changing the transaction request if you expects the ID to remain the same. + +<<< @/../../docs-snippets/src/guide/cookbook/transferring-assets.test.ts#transferring-assets-3{ts:line-numbers} + +## Transferring Assets To Contracts + +When transferring assets to a deployed contract, we use the `transferToContract` method. This method closely mirrors the `account.transfer` method in terms of parameter structure. + +However, instead of supplying the target wallet's address, as done in `destination.address` for the transfer method, we need to provide an instance of [Address](../types/address.md) created from the deployed contract id. + +If you have the [Contract](../contracts/) instance of the deployed contract, you can simply use its `id` property. However, if the contract was deployed with `forc deploy` or not by you, you will likely only have its ID in a hex string format. In such cases, you can create an [Address](../types/address.md) instance from the contract ID using `Address.fromAddressOrString('0x123...')`. + +### Example + +Here's an example demonstrating how to use `transferToContract`: + +<<< @/../../docs-snippets/src/guide/cookbook/transferring-assets.test.ts#transferring-assets-4{ts:line-numbers} + +Remember to always invoke the `waitForResult()` function on the transaction response. This ensures that the transaction has been successfully mined before proceeding. diff --git a/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md b/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md index b620dc80953..5a14c559e73 100644 --- a/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md +++ b/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md @@ -38,6 +38,14 @@ Note the method transfer has two parameters: the recipient's address and the int Once the predicate resolves with a return value `true` based on its predefined condition, our predicate successfully spends its funds by means of a transfer to a desired wallet. +--- + +In a similar approach, you can use the `createTransfer` method, which returns a `ScriptTransactionRequest`. Then, we can submit this transaction request by calling the `sendTransaction` method. + +This can be useful if you need the transaction ID before actually submitting it to the node. + +<<< @/../../docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts#send-and-spend-funds-from-predicates-8{ts:line-numbers} + ## Spending Entire Predicate Held Amount Trying to forward the entire amount held by the predicate results in an error because no funds are left to cover the transaction fees. Attempting this will result in an error message like: diff --git a/apps/docs/src/guide/wallets/transferring-assets.md b/apps/docs/src/guide/wallets/transferring-assets.md deleted file mode 100644 index aed72d51286..00000000000 --- a/apps/docs/src/guide/wallets/transferring-assets.md +++ /dev/null @@ -1,35 +0,0 @@ -# Asset Transfers - -This documentation provides a guide on how to transfer assets between wallets and to contracts using the SDK. - -## Transferring Assets Between Wallets - -The `wallet.transfer` function is used to initiate a transaction that transfers an asset from one wallet to another. This function requires three parameters: - -1. The receiver's wallet address. -2. The amount of the asset to be transferred. -3. The ID of the asset to be transferred. - -Upon execution, this function returns a promise that resolves to a transaction response. To wait for the transaction to be mined, call `response.wait()`. - -### Example - -Here is an illustration on how to use the `wallet.transfer` function: - -<<< @/../../docs-snippets/src/guide/wallets/transferring-assets.test.ts#transferring-assets-1{ts:line-numbers} - -## Transferring Assets To Contracts - -When transferring assets to a deployed contract, we use the `transferToContract` method. This method closely mirrors the `wallet.transfer` method in terms of parameter structure. - -However, instead of supplying the target wallet's address, as done in `myWallet.address` for the transfer method, we need to provide an instance of [Address](../types/address.md) created from the deployed contract id. - -If you have the [Contract](../contracts/) instance of the deployed contract, you can simply use its `id` property. However, if the contract was deployed with `forc deploy` or not by you, you will likely only have its ID in a hex string format. In such cases, you can create an [Address](../types/address.md) instance from the contract ID using `Address.fromAddressOrString('0x123...')`. - -### Example - -Here's an example demonstrating how to use `transferToContract`: - -<<< @/../../docs-snippets/src/guide/wallets/transferring-assets.test.ts#transferring-assets-2{ts:line-numbers} - -Remember to always invoke the `waitForResult()` function on the transaction response. This ensures that the transaction has been successfully mined before proceeding. diff --git a/internal/check-imports/src/references.ts b/internal/check-imports/src/references.ts index efdba621329..edbac0cef6c 100644 --- a/internal/check-imports/src/references.ts +++ b/internal/check-imports/src/references.ts @@ -6,7 +6,7 @@ import { Address } from '@fuel-ts/address'; import { BaseAssetId } from '@fuel-ts/address/configs'; import { ContractFactory } from '@fuel-ts/contract'; import { encrypt, decrypt } from '@fuel-ts/crypto'; -import { hashTransaction, hashMessage } from '@fuel-ts/hasher'; +import { hashMessage } from '@fuel-ts/hasher'; import { HDWallet } from '@fuel-ts/hdwallet'; import { AbstractPredicate } from '@fuel-ts/interfaces'; import { BN } from '@fuel-ts/math'; @@ -84,7 +84,6 @@ log(createConfig); /** * hasher */ -log(hashTransaction); log(hashMessage); /** diff --git a/packages/hasher/package.json b/packages/hasher/package.json index 02aac8e63b0..3cb9f7aca94 100644 --- a/packages/hasher/package.json +++ b/packages/hasher/package.json @@ -28,8 +28,6 @@ "@fuel-ts/address": "workspace:*", "@fuel-ts/crypto": "workspace:*", "@fuel-ts/math": "workspace:*", - "@fuel-ts/providers": "workspace:*", - "@fuel-ts/transactions": "workspace:*", "@fuel-ts/utils": "workspace:*", "ethers": "^6.7.1", "ramda": "^0.29.0" diff --git a/packages/hasher/src/hasher.test.ts b/packages/hasher/src/hasher.test.ts index dceaf19830c..5f3b9d2e22e 100644 --- a/packages/hasher/src/hasher.test.ts +++ b/packages/hasher/src/hasher.test.ts @@ -1,9 +1,6 @@ -import { bn } from '@fuel-ts/math'; -import { transactionRequestify } from '@fuel-ts/providers'; import signMessageTest from '@fuel-ts/testcases/src/signMessage.json'; -import signTransactionTest from '@fuel-ts/testcases/src/signTransaction.json'; -import { hashMessage, hash, hashTransaction } from './hasher'; +import { hashMessage, hash } from './hasher'; describe('Hasher', () => { it('Hash message', () => { @@ -15,40 +12,4 @@ describe('Hasher', () => { '0xf5ca38f748a1d6eaf726b8a42fb575c3c71f1864a8143301782de13da2d9202b' ); }); - - it('Hash script transaction request', () => { - const transactionRequest = signTransactionTest.transaction; - - expect(hashTransaction(transactionRequest, 0)).toEqual(signTransactionTest.hashedTransaction); - }); - - it('Hash script transaction with predicateGas', () => { - const transactionRequest = transactionRequestify({ - type: 0, - gasPrice: '0x1', - gasLimit: '0x1dcd6500', - inputs: [ - { - type: 2, - amount: '0x0', - sender: '0x00000000000000000000000059f2f1fcfe2474fd5f0b9ba1e73ca90b143eb8d0', - recipient: '0x809f80a984a60d641d4a6d284e3969ff380eb478e587bcba0ed1a3f0ef271ede', - witnessIndex: 0, - data: '0x3b42028d86539b607afeaf8d32501fa03c494ecad7e65a16a7a8ebc264f4b67a000000000000000000000000dadd1125b8df98a66abd5eb302c0d9ca5a061dc200000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b906456bdaffaf74fe03521754ac445d148033c0c6acf7d593132c43f92fdbc7324c0000000000000000000000000000000000000000000000008ac7230489e80000', - nonce: '0x0000000000000000000000000000000000000000000000000000000000000002', - predicate: - '0x1a405000910000206144000b6148000540411480504cc04c72580020295134165b501012615c000772680002595d7001616171015b61a0106165711a5b6400125b5c100b2404000024000000664e627bfc0db0bfa8f182efc913b552681143e328b555d9697c40ad0eb527ad', - predicateGasUsed: bn(25), - }, - ], - outputs: [], - witnesses: [], - script: '0x', - scriptData: '0x', - }); - - expect(hashTransaction(transactionRequest, 0)).toEqual( - '0xc0f5e25f37294c5b0218b377cae9622df03a05dc540374827c9e7a7f65560842' - ); - }); }); diff --git a/packages/hasher/src/hasher.ts b/packages/hasher/src/hasher.ts index 685cdf9d401..42b7dd45a04 100644 --- a/packages/hasher/src/hasher.ts +++ b/packages/hasher/src/hasher.ts @@ -1,12 +1,6 @@ -import { ZeroBytes32 } from '@fuel-ts/address/configs'; import { bufferFromString } from '@fuel-ts/crypto'; -import { bn } from '@fuel-ts/math'; -import type { TransactionRequestLike } from '@fuel-ts/providers'; -import { transactionRequestify } from '@fuel-ts/providers'; -import { OutputType, InputType, TransactionCoder, TransactionType } from '@fuel-ts/transactions'; import type { BytesLike } from 'ethers'; -import { sha256, concat } from 'ethers'; -import { clone } from 'ramda'; +import { sha256 } from 'ethers'; /** * hash string messages with sha256 @@ -29,91 +23,6 @@ export function uint64ToBytesBE(value: number): Uint8Array { return new Uint8Array(dataView.buffer); } -/** - * Hash transaction request with sha256. [Read more](https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/identifiers.md#transaction-id) - * - * @param transactionRequest - Transaction request to be hashed - * @returns sha256 hash of the transaction - */ -export function hashTransaction(transactionRequestLike: TransactionRequestLike, chainId: number) { - const transactionRequest = transactionRequestify(transactionRequestLike); - // Return a new transaction object without references to the original transaction request - const transaction = transactionRequest.toTransaction(); - - if (transaction.type === TransactionType.Script) { - transaction.receiptsRoot = ZeroBytes32; - } - - // Zero out input fields - transaction.inputs = transaction.inputs.map((input) => { - const inputClone = clone(input); - - switch (inputClone.type) { - // Zero out on signing: txPointer, predicateGasUsed - case InputType.Coin: { - inputClone.txPointer = { - blockHeight: 0, - txIndex: 0, - }; - inputClone.predicateGasUsed = bn(0); - return inputClone; - } - // Zero out on signing: predicateGasUsed - case InputType.Message: { - inputClone.predicateGasUsed = bn(0); - return inputClone; - } - // Zero out on signing: txID, outputIndex, balanceRoot, stateRoot, and txPointer - case InputType.Contract: { - inputClone.txPointer = { - blockHeight: 0, - txIndex: 0, - }; - inputClone.txID = ZeroBytes32; - inputClone.outputIndex = 0; - inputClone.balanceRoot = ZeroBytes32; - inputClone.stateRoot = ZeroBytes32; - return inputClone; - } - default: - return inputClone; - } - }); - // Zero out output fields - transaction.outputs = transaction.outputs.map((output) => { - const outputClone = clone(output); - - switch (outputClone.type) { - // Zero out on signing: balanceRoot, stateRoot - case OutputType.Contract: { - outputClone.balanceRoot = ZeroBytes32; - outputClone.stateRoot = ZeroBytes32; - return outputClone; - } - // Zero out on signing: amount - case OutputType.Change: { - outputClone.amount = bn(0); - return outputClone; - } - // Zero out on signing: amount, to and assetId - case OutputType.Variable: { - outputClone.to = ZeroBytes32; - outputClone.amount = bn(0); - outputClone.assetId = ZeroBytes32; - return outputClone; - } - default: - return outputClone; - } - }); - transaction.witnessesCount = 0; - transaction.witnesses = []; - - const chainIdBytes = uint64ToBytesBE(chainId); - const concatenatedData = concat([chainIdBytes, new TransactionCoder().encode(transaction)]); - return sha256(concatenatedData); -} - /** * wrap sha256 * diff --git a/packages/predicate/src/predicate.ts b/packages/predicate/src/predicate.ts index fe61b61a9e2..f425290b620 100644 --- a/packages/predicate/src/predicate.ts +++ b/packages/predicate/src/predicate.ts @@ -7,18 +7,21 @@ import { SCRIPT_FIXED_SIZE, } from '@fuel-ts/abi-coder'; import { Address } from '@fuel-ts/address'; +import { BaseAssetId } from '@fuel-ts/address/configs'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; -import type { AbstractPredicate } from '@fuel-ts/interfaces'; +import type { AbstractAddress, AbstractPredicate, BytesLike } from '@fuel-ts/interfaces'; +import type { BigNumberish } from '@fuel-ts/math'; import type { CallResult, Provider, + TransactionRequest, TransactionRequestLike, TransactionResponse, } from '@fuel-ts/providers'; import { transactionRequestify, BaseTransactionRequest } from '@fuel-ts/providers'; import { ByteArrayCoder, InputType } from '@fuel-ts/transactions'; +import type { TxParamsType } from '@fuel-ts/wallet'; import { Account } from '@fuel-ts/wallet'; -import type { BytesLike } from 'ethers'; import { getBytesCopy, hexlify } from 'ethers'; import { getPredicateRoot } from './utils'; @@ -32,12 +35,10 @@ export class Predicate extends Account implements Abs predicateArgs: ARGS = [] as unknown as ARGS; interface?: Interface; - // TODO: Since provider is no longer optional, we can maybe remove `chainId` from the constructor. /** * Creates an instance of the Predicate class. * * @param bytes - The bytes of the predicate. - * @param chainId - The chain ID for which the predicate is used. * @param provider - The provider used to interact with the blockchain. * @param jsonAbi - The JSON ABI of the predicate. * @param configurableConstants - Optional configurable constants for the predicate. @@ -76,13 +77,36 @@ export class Predicate extends Account implements Abs // eslint-disable-next-line no-param-reassign input.predicate = this.bytes; // eslint-disable-next-line no-param-reassign - input.predicateData = this.#getPredicateData(policies.length); + input.predicateData = this.getPredicateData(policies.length); } }); return request; } + /** + * A helper that creates a transfer transaction request and returns it. + * + * @param destination - The address of the destination. + * @param amount - The amount of coins to transfer. + * @param assetId - The asset ID of the coins to transfer. + * @param txParams - The transaction parameters (gasLimit, gasPrice, maturity). + * @returns A promise that resolves to the prepared transaction request. + */ + async createTransfer( + /** Address of the destination */ + destination: AbstractAddress, + /** Amount of coins */ + amount: BigNumberish, + /** Asset ID of coins */ + assetId: BytesLike = BaseAssetId, + /** Tx Params */ + txParams: TxParamsType = {} + ): Promise { + const request = await super.createTransfer(destination, amount, assetId, txParams); + return this.populateTransactionPredicateData(request); + } + /** * Sends a transaction with the populated predicate data. * @@ -117,7 +141,7 @@ export class Predicate extends Account implements Abs return this; } - #getPredicateData(policiesLength: number): Uint8Array { + private getPredicateData(policiesLength: number): Uint8Array { if (!this.predicateArgs.length) { return new Uint8Array(); } diff --git a/packages/program/src/functions/base-invocation-scope.ts b/packages/program/src/functions/base-invocation-scope.ts index 725d1aa35cb..b9857c7b78e 100644 --- a/packages/program/src/functions/base-invocation-scope.ts +++ b/packages/program/src/functions/base-invocation-scope.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { InputValue } from '@fuel-ts/abi-coder'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; -import { hashTransaction } from '@fuel-ts/hasher'; import type { AbstractContract, AbstractProgram } from '@fuel-ts/interfaces'; import type { BN } from '@fuel-ts/math'; import { bn, toNumber } from '@fuel-ts/math'; @@ -385,6 +384,6 @@ export class BaseInvocationScope { const chainIdToHash = chainId ?? (await this.getProvider().getChainId()); const transactionRequest = await this.getTransactionRequest(); - return hashTransaction(transactionRequest, chainIdToHash); + return transactionRequest.getTransactionId(chainIdToHash); } } diff --git a/packages/providers/package.json b/packages/providers/package.json index d85afa221ae..e1b0359e227 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -34,6 +34,7 @@ "@fuel-ts/errors": "workspace:*", "@fuel-ts/interfaces": "workspace:*", "@fuel-ts/math": "workspace:*", + "@fuel-ts/hasher": "workspace:*", "@fuel-ts/transactions": "workspace:*", "ethers": "^6.7.1", "@fuel-ts/versions": "workspace:*", diff --git a/packages/providers/src/transaction-request/create-transaction-request.ts b/packages/providers/src/transaction-request/create-transaction-request.ts index e1c074f000e..242409a9fa2 100644 --- a/packages/providers/src/transaction-request/create-transaction-request.ts +++ b/packages/providers/src/transaction-request/create-transaction-request.ts @@ -8,6 +8,7 @@ import type { BytesLike } from 'ethers'; import type { GqlGasCosts } from '../__generated__/operations'; import { calculateMetadataGasForTxCreate } from '../utils/gas'; +import { hashTransaction } from './hash-transaction'; import type { ContractCreatedTransactionRequestOutput } from './output'; import type { TransactionRequestStorageSlot } from './storage-slot'; import { storageSlotify } from './storage-slot'; @@ -95,6 +96,17 @@ export class CreateTransactionRequest extends BaseTransactionRequest { ); } + /** + * Gets the Transaction Request by hashing the transaction. + * + * @param chainId - The chain ID. + * + * @returns - A hash of the transaction, which is the transaction ID. + */ + getTransactionId(chainId: number): string { + return hashTransaction(this, chainId); + } + /** * Adds a contract created output to the transaction request. * diff --git a/packages/providers/src/transaction-request/hash-transaction.test.ts b/packages/providers/src/transaction-request/hash-transaction.test.ts new file mode 100644 index 00000000000..25364dbcebe --- /dev/null +++ b/packages/providers/src/transaction-request/hash-transaction.test.ts @@ -0,0 +1,38 @@ +import { bn } from '@fuel-ts/math'; +import { clone } from 'ramda'; + +import { SCRIPT_TX_REQUEST } from '../../test/fixtures/transaction-request'; + +import { hashTransaction } from './hash-transaction'; + +describe('hashTransaction', () => { + it('Hash script transaction request', () => { + expect(hashTransaction(SCRIPT_TX_REQUEST, 0)).toEqual( + '0x7645fa2154ee610469ebc876d0cb7b6fcf390fb97f2c6b88a2344cc23533fa39' + ); + }); + + it('Hash script transaction with predicateGas', () => { + const txRequest = clone(SCRIPT_TX_REQUEST); + + txRequest.inputs = [ + ...txRequest.inputs, + { + type: 2, + amount: '0x0', + sender: '0x00000000000000000000000059f2f1fcfe2474fd5f0b9ba1e73ca90b143eb8d0', + recipient: '0x809f80a984a60d641d4a6d284e3969ff380eb478e587bcba0ed1a3f0ef271ede', + witnessIndex: 0, + data: '0x3b42028d86539b607afeaf8d32501fa03c494ecad7e65a16a7a8ebc264f4b67a000000000000000000000000dadd1125b8df98a66abd5eb302c0d9ca5a061dc200000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b906456bdaffaf74fe03521754ac445d148033c0c6acf7d593132c43f92fdbc7324c0000000000000000000000000000000000000000000000008ac7230489e80000', + nonce: '0x0000000000000000000000000000000000000000000000000000000000000002', + predicate: + '0x1a405000910000206144000b6148000540411480504cc04c72580020295134165b501012615c000772680002595d7001616171015b61a0106165711a5b6400125b5c100b2404000024000000664e627bfc0db0bfa8f182efc913b552681143e328b555d9697c40ad0eb527ad', + predicateGasUsed: bn(25), + }, + ]; + + expect(hashTransaction(txRequest, 0)).toEqual( + '0xf3f6ef8a9e6a495fbe4998d8cb197550aecf1eb9e89ce10cf13a8b03bd4dfb6a' + ); + }); +}); diff --git a/packages/providers/src/transaction-request/hash-transaction.ts b/packages/providers/src/transaction-request/hash-transaction.ts new file mode 100644 index 00000000000..e7b4ea05590 --- /dev/null +++ b/packages/providers/src/transaction-request/hash-transaction.ts @@ -0,0 +1,91 @@ +import { ZeroBytes32 } from '@fuel-ts/address/configs'; +import { uint64ToBytesBE } from '@fuel-ts/hasher'; +import { bn } from '@fuel-ts/math'; +import { TransactionType, InputType, OutputType, TransactionCoder } from '@fuel-ts/transactions'; +import { concat, sha256 } from 'ethers'; +import { clone } from 'ramda'; + +import type { TransactionRequest } from './types'; + +/** + * Hash transaction request with sha256. [Read more](https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/identifiers.md#transaction-id) + * + * @param transactionRequest - Transaction request to be hashed + * @returns sha256 hash of the transaction + */ +export function hashTransaction(transactionRequest: TransactionRequest, chainId: number) { + const transaction = transactionRequest.toTransaction(); + + if (transaction.type === TransactionType.Script) { + transaction.receiptsRoot = ZeroBytes32; + } + + // Zero out input fields + transaction.inputs = transaction.inputs.map((input) => { + const inputClone = clone(input); + + switch (inputClone.type) { + // Zero out on signing: txPointer, predicateGasUsed + case InputType.Coin: { + inputClone.txPointer = { + blockHeight: 0, + txIndex: 0, + }; + inputClone.predicateGasUsed = bn(0); + return inputClone; + } + // Zero out on signing: predicateGasUsed + case InputType.Message: { + inputClone.predicateGasUsed = bn(0); + return inputClone; + } + // Zero out on signing: txID, outputIndex, balanceRoot, stateRoot, and txPointer + case InputType.Contract: { + inputClone.txPointer = { + blockHeight: 0, + txIndex: 0, + }; + inputClone.txID = ZeroBytes32; + inputClone.outputIndex = 0; + inputClone.balanceRoot = ZeroBytes32; + inputClone.stateRoot = ZeroBytes32; + return inputClone; + } + default: + return inputClone; + } + }); + // Zero out output fields + transaction.outputs = transaction.outputs.map((output) => { + const outputClone = clone(output); + + switch (outputClone.type) { + // Zero out on signing: balanceRoot, stateRoot + case OutputType.Contract: { + outputClone.balanceRoot = ZeroBytes32; + outputClone.stateRoot = ZeroBytes32; + return outputClone; + } + // Zero out on signing: amount + case OutputType.Change: { + outputClone.amount = bn(0); + return outputClone; + } + // Zero out on signing: amount, to and assetId + case OutputType.Variable: { + outputClone.to = ZeroBytes32; + outputClone.amount = bn(0); + outputClone.assetId = ZeroBytes32; + return outputClone; + } + default: + return outputClone; + } + }); + transaction.witnessesCount = 0; + transaction.witnesses = []; + + const chainIdBytes = uint64ToBytesBE(chainId); + const concatenatedData = concat([chainIdBytes, new TransactionCoder().encode(transaction)]); + return sha256(concatenatedData); +} diff --git a/packages/providers/src/transaction-request/script-transaction-request.ts b/packages/providers/src/transaction-request/script-transaction-request.ts index 47e0eee5525..150d79511be 100644 --- a/packages/providers/src/transaction-request/script-transaction-request.ts +++ b/packages/providers/src/transaction-request/script-transaction-request.ts @@ -14,6 +14,7 @@ import type { GqlGasCosts } from '../__generated__/operations'; import type { ChainInfo } from '../provider'; import { calculateMetadataGasForTxScript, getMaxGas } from '../utils/gas'; +import { hashTransaction } from './hash-transaction'; import type { ContractTransactionRequestInput } from './input'; import type { ContractTransactionRequestOutput, VariableTransactionRequestOutput } from './output'; import { returnZeroScript } from './scripts'; @@ -193,6 +194,17 @@ export class ScriptTransactionRequest extends BaseTransactionRequest { return this; } + /** + * Gets the Transaction Request by hashing the transaction. + * + * @param chainId - The chain ID. + * + * @returns - A hash of the transaction, which is the transaction ID. + */ + getTransactionId(chainId: number): string { + return hashTransaction(this, chainId); + } + /** * Sets the data for the transaction request. * diff --git a/packages/providers/src/transaction-request/transaction-request.ts b/packages/providers/src/transaction-request/transaction-request.ts index da824b0e62b..83092b4de8a 100644 --- a/packages/providers/src/transaction-request/transaction-request.ts +++ b/packages/providers/src/transaction-request/transaction-request.ts @@ -612,6 +612,15 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi return coinsQuantities; } + /** + * Gets the Transaction Request by hashing the transaction. + * + * @param chainId - The chain ID. + * + * @returns - A hash of the transaction, which is the transaction ID. + */ + abstract getTransactionId(chainId: number): string; + /** * Return the minimum amount in native coins required to create * a transaction. diff --git a/packages/providers/test/fixtures/transaction-request.ts b/packages/providers/test/fixtures/transaction-request.ts new file mode 100644 index 00000000000..cf2f7657b09 --- /dev/null +++ b/packages/providers/test/fixtures/transaction-request.ts @@ -0,0 +1,35 @@ +import { ScriptTransactionRequest } from '../../src/transaction-request/script-transaction-request'; + +export const SCRIPT_TX_REQUEST = new ScriptTransactionRequest({ + gasLimit: 10_000, + script: '0x24400000', + scriptData: Uint8Array.from([]), + gasPrice: 10, + maxFee: 90000, + maturity: 0, + witnessLimit: 3000, + inputs: [ + { + type: 0, + id: '0x000000000000000000000000000000000000000000000000000000000000000000', + assetId: '0x0000000000000000000000000000000000000000000000000000000000000000', + amount: '0x989680', + owner: '0xf1e92c42b90934aa6372e30bc568a326f6e66a1a0288595e6e3fbd392a4f3e6e', + maturity: 0, + txPointer: '0x00000000000000000000000000000000', + witnessIndex: 0, + predicate: '0x', + predicateData: '0x', + predicateGasUsed: '0x20', + }, + ], + outputs: [ + { + type: 0, + to: '0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077', + assetId: '0x0000000000000000000000000000000000000000000000000000000000000000', + amount: 1, + }, + ], + witnesses: ['0x'], +}); diff --git a/packages/wallet/src/account.ts b/packages/wallet/src/account.ts index d6d10f9be65..a0c719b6633 100644 --- a/packages/wallet/src/account.ts +++ b/packages/wallet/src/account.ts @@ -225,15 +225,15 @@ export class Account extends AbstractAccount { } /** - * Transfers coins to a destination address. + * A helper that creates a transfer transaction request and returns it. * * @param destination - The address of the destination. * @param amount - The amount of coins to transfer. * @param assetId - The asset ID of the coins to transfer. * @param txParams - The transaction parameters (gasLimit, gasPrice, maturity). - * @returns A promise that resolves to the transaction response. + * @returns A promise that resolves to the prepared transaction request. */ - async transfer( + async createTransfer( /** Address of the destination */ destination: AbstractAddress, /** Amount of coins */ @@ -242,19 +242,36 @@ export class Account extends AbstractAccount { assetId: BytesLike = BaseAssetId, /** Tx Params */ txParams: TxParamsType = {} - ): Promise { + ): Promise { const { minGasPrice } = this.provider.getGasConfig(); - const params = { gasPrice: minGasPrice, ...txParams }; - const request = new ScriptTransactionRequest(params); - request.addCoinOutput(destination, amount, assetId); - const { maxFee, requiredQuantities } = await this.provider.getTransactionCost(request); - await this.fund(request, requiredQuantities, maxFee); + return request; + } + /** + * Transfers coins to a destination address. + * + * @param destination - The address of the destination. + * @param amount - The amount of coins to transfer. + * @param assetId - The asset ID of the coins to transfer. + * @param txParams - The transaction parameters (gasLimit, gasPrice, maturity). + * @returns A promise that resolves to the transaction response. + */ + async transfer( + /** Address of the destination */ + destination: AbstractAddress, + /** Amount of coins */ + amount: BigNumberish, + /** Asset ID of coins */ + assetId: BytesLike = BaseAssetId, + /** Tx Params */ + txParams: TxParamsType = {} + ): Promise { + const request = await this.createTransfer(destination, amount, assetId, txParams); return this.sendTransaction(request); } diff --git a/packages/wallet/src/base-unlocked-wallet.ts b/packages/wallet/src/base-unlocked-wallet.ts index 79f54c72feb..d26627465de 100644 --- a/packages/wallet/src/base-unlocked-wallet.ts +++ b/packages/wallet/src/base-unlocked-wallet.ts @@ -1,4 +1,4 @@ -import { hashMessage, hashTransaction } from '@fuel-ts/hasher'; +import { hashMessage } from '@fuel-ts/hasher'; import type { TransactionResponse, TransactionRequestLike, @@ -81,8 +81,8 @@ export class BaseWalletUnlocked extends Account { */ async signTransaction(transactionRequestLike: TransactionRequestLike): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); - const chainId = (await this.provider.getChain()).consensusParameters.chainId.toNumber(); - const hashedTransaction = hashTransaction(transactionRequest, chainId); + const chainId = this.provider.getChain().consensusParameters.chainId.toNumber(); + const hashedTransaction = transactionRequest.getTransactionId(chainId); const signature = await this.signer().sign(hashedTransaction); return signature; diff --git a/packages/wallet/src/transfer.test.ts b/packages/wallet/src/transfer.test.ts index 0bbb3d78118..9c62e9d0fef 100644 --- a/packages/wallet/src/transfer.test.ts +++ b/packages/wallet/src/transfer.test.ts @@ -35,6 +35,25 @@ describe('Wallet', () => { expect(receiverBalances).toEqual([{ assetId: BaseAssetId, amount: bn(1) }]); }); + it('can create transfer request just fine', async () => { + const sender = await generateTestWallet(provider, [[500_000, BaseAssetId]]); + const receiver = await generateTestWallet(provider); + + const request = await sender.createTransfer(receiver.address, 1, BaseAssetId, { + gasPrice, + gasLimit: 10_000, + }); + + const response = await sender.sendTransaction(request); + await response.wait(); + + const senderBalances = await sender.getBalances(); + const receiverBalances = await receiver.getBalances(); + + expect(senderBalances).toEqual([{ assetId: BaseAssetId, amount: bn(499921) }]); + expect(receiverBalances).toEqual([{ assetId: BaseAssetId, amount: bn(1) }]); + }); + it('can transfer with custom TX Params', async () => { const sender = await generateTestWallet(provider, [[500_000, BaseAssetId]]); const receiver = await generateTestWallet(provider); diff --git a/packages/wallet/src/wallet-unlocked.test.ts b/packages/wallet/src/wallet-unlocked.test.ts index 60a2ff00e8f..386eead120b 100644 --- a/packages/wallet/src/wallet-unlocked.test.ts +++ b/packages/wallet/src/wallet-unlocked.test.ts @@ -1,14 +1,14 @@ import { randomBytes } from '@fuel-ts/crypto'; -import { hashMessage, hashTransaction } from '@fuel-ts/hasher'; +import { hashMessage } from '@fuel-ts/hasher'; import type { CallResult, TransactionResponse, TransactionRequestLike } from '@fuel-ts/providers'; import { Provider } from '@fuel-ts/providers'; import * as providersMod from '@fuel-ts/providers'; import { Signer } from '@fuel-ts/signer'; -import sendTransactionTest from '@fuel-ts/testcases/src/sendTransaction.json'; import signMessageTest from '@fuel-ts/testcases/src/signMessage.json'; -import signTransactionTest from '@fuel-ts/testcases/src/signTransaction.json'; import type { BytesLike } from 'ethers'; +import { SCRIPT_TX_REQUEST, SIGNED_TX, PRIVATE_KEY } from '../test/fixtures/wallet-unlocked'; + import { BaseWalletUnlocked } from './base-unlocked-wallet'; import { FUEL_NETWORK_URL } from './configs'; import * as keystoreWMod from './keystore-wallet'; @@ -48,42 +48,39 @@ describe('WalletUnlocked', () => { // #region wallet-transaction-signing // #context import { WalletUnlocked, hashMessage, Signer} from 'fuels'; const provider = await Provider.create(FUEL_NETWORK_URL); - const wallet = new WalletUnlocked(signTransactionTest.privateKey, provider); - const transactionRequest = signTransactionTest.transaction; - const signedTransaction = await wallet.signTransaction(transactionRequest); - const chainId = (await wallet.provider.getChain()).consensusParameters.chainId.toNumber(); + const wallet = new WalletUnlocked(PRIVATE_KEY, provider); + const signedTransaction = await wallet.signTransaction(SCRIPT_TX_REQUEST); + const chainId = wallet.provider.getChain().consensusParameters.chainId.toNumber(); const verifiedAddress = Signer.recoverAddress( - hashTransaction(transactionRequest, chainId), + SCRIPT_TX_REQUEST.getTransactionId(chainId), signedTransaction ); - expect(signedTransaction).toEqual(signTransactionTest.signedTransaction); + expect(signedTransaction).toEqual(SIGNED_TX); expect(verifiedAddress).toEqual(wallet.address); // #endregion wallet-transaction-signing }); it('Populate transaction witnesses signature using wallet instance', async () => { const provider = await Provider.create(FUEL_NETWORK_URL); - const wallet = new WalletUnlocked(signTransactionTest.privateKey, provider); - const transactionRequest = signTransactionTest.transaction; - const signedTransaction = await wallet.signTransaction(transactionRequest); + const wallet = new WalletUnlocked(PRIVATE_KEY, provider); + const signedTransaction = await wallet.signTransaction(SCRIPT_TX_REQUEST); const populatedTransaction = - await wallet.populateTransactionWitnessesSignature(transactionRequest); + await wallet.populateTransactionWitnessesSignature(SCRIPT_TX_REQUEST); expect(populatedTransaction.witnesses?.[0]).toBe(signedTransaction); }); it('Populate transaction multi-witnesses signature using wallet instance', async () => { const provider = await Provider.create(FUEL_NETWORK_URL); - const wallet = new WalletUnlocked(signTransactionTest.privateKey, provider); + const wallet = new WalletUnlocked(PRIVATE_KEY, provider); const privateKey = randomBytes(32); const otherWallet = new WalletUnlocked(privateKey, provider); - const transactionRequest = signTransactionTest.transaction; - const signedTransaction = await wallet.signTransaction(transactionRequest); - const otherSignedTransaction = await otherWallet.signTransaction(transactionRequest); + const signedTransaction = await wallet.signTransaction(SCRIPT_TX_REQUEST); + const otherSignedTransaction = await otherWallet.signTransaction(SCRIPT_TX_REQUEST); const populatedTransaction = await wallet.populateTransactionWitnessesSignature({ - ...transactionRequest, - witnesses: [...transactionRequest.witnesses, otherSignedTransaction], + ...SCRIPT_TX_REQUEST, + witnesses: [...SCRIPT_TX_REQUEST.witnesses, otherSignedTransaction], }); expect(populatedTransaction.witnesses?.length).toBe(2); @@ -93,8 +90,7 @@ describe('WalletUnlocked', () => { it('Check if send transaction adds signature using wallet instance', async () => { const provider = await Provider.create(FUEL_NETWORK_URL); - const wallet = new WalletUnlocked(signTransactionTest.privateKey, provider); - const transactionRequest = sendTransactionTest.transaction; + const wallet = new WalletUnlocked(PRIVATE_KEY, provider); let signature: BytesLike | undefined; // Intercept Provider.sendTransaction to collect signature const spy = jest @@ -105,7 +101,7 @@ describe('WalletUnlocked', () => { }); // Call send transaction should populate signature field - await wallet.sendTransaction(transactionRequest); + await wallet.sendTransaction(SCRIPT_TX_REQUEST); // Provider sendTransaction should be called expect(spy).toBeCalled(); @@ -202,12 +198,12 @@ describe('WalletUnlocked', () => { const transactionRequestLike: TransactionRequestLike = { type: providersMod.TransactionType.Script, }; - const transactionRequest = new ScriptTransactionRequest(); + const transactionReq = new ScriptTransactionRequest(); const callResult = 'callResult' as unknown as CallResult; const transactionRequestify = jest .spyOn(providersMod, 'transactionRequestify') - .mockImplementation(() => transactionRequest); + .mockImplementation(() => transactionReq); const estimateTxDependencies = jest .spyOn(providersMod.Provider.prototype, 'estimateTxDependencies') @@ -219,7 +215,7 @@ describe('WalletUnlocked', () => { const populateTransactionWitnessesSignatureSpy = jest .spyOn(BaseWalletUnlocked.prototype, 'populateTransactionWitnessesSignature') - .mockImplementationOnce(() => Promise.resolve(transactionRequest)); + .mockImplementationOnce(() => Promise.resolve(transactionReq)); const provider = await Provider.create(FUEL_NETWORK_URL); @@ -235,10 +231,10 @@ describe('WalletUnlocked', () => { expect(transactionRequestify.mock.calls[0][0]).toEqual(transactionRequestLike); expect(estimateTxDependencies.mock.calls.length).toBe(1); - expect(estimateTxDependencies.mock.calls[0][0]).toEqual(transactionRequest); + expect(estimateTxDependencies.mock.calls[0][0]).toEqual(transactionReq); expect(populateTransactionWitnessesSignatureSpy.mock.calls.length).toBe(1); - expect(populateTransactionWitnessesSignatureSpy.mock.calls[0][0]).toEqual(transactionRequest); + expect(populateTransactionWitnessesSignatureSpy.mock.calls[0][0]).toEqual(transactionReq); expect(call.mock.calls.length).toBe(1); }); diff --git a/packages/wallet/test/fixtures/wallet-unlocked.ts b/packages/wallet/test/fixtures/wallet-unlocked.ts new file mode 100644 index 00000000000..1e8b8a19322 --- /dev/null +++ b/packages/wallet/test/fixtures/wallet-unlocked.ts @@ -0,0 +1,43 @@ +import { ScriptTransactionRequest } from '@fuel-ts/providers'; + +export const SCRIPT_TX_REQUEST = new ScriptTransactionRequest({ + gasLimit: 5_000, + script: '0x', + scriptData: Uint8Array.from([]), + gasPrice: 5, + maxFee: 20_000, + maturity: 0, + witnessLimit: 5000, + inputs: [ + { + type: 0, + id: '0x000000000000000000000000000000000000000000000000000000000000000000', + assetId: '0x0000000000000000000000000000000000000000000000000000000000000000', + amount: '0x989680', + owner: '0xf1e92c42b90934aa6372e30bc568a326f6e66a1a0288595e6e3fbd392a4f3e6e', + maturity: 0, + txPointer: '0x00000000000000000000000000000000', + witnessIndex: 0, + predicate: '0x', + predicateData: '0x', + predicateGasUsed: '0x20', + }, + ], + outputs: [ + { + type: 0, + to: '0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077', + assetId: '0x0000000000000000000000000000000000000000000000000000000000000000', + amount: 1, + }, + ], + witnesses: ['0x'], +}); + +export const PRIVATE_KEY = '0x5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1'; +export const PUBLIC_KEY = + '0x2f34bc0df4db0ec391792cedb05768832b49b1aa3a2dd8c30054d1af00f67d00b74b7acbbf3087c8e0b1a4c343db50aa471d21f278ff5ce09f07795d541fb47e'; +export const ADDRESS = '0xf1e92c42b90934aa6372e30bc568a326f6e66a1a0288595e6e3fbd392a4f3e6e'; +export const HASHED_TX = '0x48ee795d94ea9562a3dbb9979cb44bb3dfd341eb755c378b14a3cd6886189980'; +export const SIGNED_TX = + '0x4b68db1c036e28b0ae2df25410880abaac46d5d6018b5594efa1b3854f81d937b58a609e43ac3606bfba54ca9ac03f7b076bd745b4b58f885d96a68c3006db15'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 654e67171df..2fc3a27b413 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -783,12 +783,6 @@ importers: '@fuel-ts/math': specifier: workspace:* version: link:../math - '@fuel-ts/providers': - specifier: workspace:* - version: link:../providers - '@fuel-ts/transactions': - specifier: workspace:* - version: link:../transactions '@fuel-ts/utils': specifier: workspace:* version: link:../utils @@ -965,6 +959,9 @@ importers: '@fuel-ts/errors': specifier: workspace:* version: link:../errors + '@fuel-ts/hasher': + specifier: workspace:* + version: link:../hasher '@fuel-ts/interfaces': specifier: workspace:* version: link:../interfaces