diff --git a/.changeset/friendly-cobras-occur.md b/.changeset/friendly-cobras-occur.md new file mode 100644 index 00000000000..ba60779eebc --- /dev/null +++ b/.changeset/friendly-cobras-occur.md @@ -0,0 +1,7 @@ +--- +"@fuel-ts/abi-coder": minor +"@fuel-ts/address": minor +"@fuel-ts/interfaces": minor +--- + +Improve support of Asset ID diff --git a/apps/docs-snippets/src/guide/contracts/contract-balance.test.ts b/apps/docs-snippets/src/guide/contracts/contract-balance.test.ts index 51ae2f7d95e..3c56895f17f 100644 --- a/apps/docs-snippets/src/guide/contracts/contract-balance.test.ts +++ b/apps/docs-snippets/src/guide/contracts/contract-balance.test.ts @@ -1,4 +1,4 @@ -import type { Contract } from 'fuels'; +import type { Contract, AssetId } from 'fuels'; import { Wallet, BN, BaseAssetId, Provider, FUEL_NETWORK_URL } from 'fuels'; import { DocSnippetProjectsEnum } from '../../../test/fixtures/forc-projects'; @@ -15,6 +15,7 @@ describe(__filename, () => { it('should successfully get a contract balance', async () => { // #region contract-balance-3 + // #context import type { AssetId } from 'fuels'; // #context import { Wallet, BN, BaseAssetId } from 'fuels'; const amountToForward = 40; @@ -26,8 +27,12 @@ describe(__filename, () => { const { minGasPrice, maxGasPerTx } = provider.getGasConfig(); + const asset: AssetId = { + value: BaseAssetId, + }; + await contract.functions - .transfer(amountToTransfer, BaseAssetId, recipient.address.toB256()) + .transfer(amountToTransfer, asset, recipient.address.toB256()) .callParams({ forward: [amountToForward, BaseAssetId], }) diff --git a/apps/docs-snippets/src/guide/contracts/simulate-transactions.test.ts b/apps/docs-snippets/src/guide/contracts/simulate-transactions.test.ts index e2eb059357d..7d71d099758 100644 --- a/apps/docs-snippets/src/guide/contracts/simulate-transactions.test.ts +++ b/apps/docs-snippets/src/guide/contracts/simulate-transactions.test.ts @@ -1,7 +1,11 @@ import { safeExec } from '@fuel-ts/errors/test-utils'; +import type { AssetId } from 'fuels'; import { BaseAssetId, Wallet, BN, Contract } from 'fuels'; -import { DocSnippetProjectsEnum, getDocsSnippetsForcProject } from '../../../test/fixtures/forc-projects'; +import { + DocSnippetProjectsEnum, + getDocsSnippetsForcProject, +} from '../../../test/fixtures/forc-projects'; import { createAndDeployContractFromProject } from '../../utils'; describe(__filename, () => { @@ -19,9 +23,13 @@ describe(__filename, () => { provider, }).address.toB256(); + const assetId: AssetId = { + value: BaseAssetId, + }; + // #region simulate-transactions-1 const { gasUsed } = await contract.functions - .transfer(amountToTransfer, BaseAssetId, someAddress) + .transfer(amountToTransfer, assetId, someAddress) .callParams({ forward: [amountToForward, BaseAssetId], }) diff --git a/apps/docs-snippets/src/guide/scripts/script-custom-transaction.test.ts b/apps/docs-snippets/src/guide/scripts/script-custom-transaction.test.ts index 9b1af6c0ca4..5d7d1c8ee1b 100644 --- a/apps/docs-snippets/src/guide/scripts/script-custom-transaction.test.ts +++ b/apps/docs-snippets/src/guide/scripts/script-custom-transaction.test.ts @@ -1,7 +1,10 @@ import { BN, ContractFactory, BaseAssetId, ScriptTransactionRequest } from 'fuels'; import type { CoinQuantityLike, Contract, WalletUnlocked } from 'fuels'; -import { DocSnippetProjectsEnum, getDocsSnippetsForcProject } from '../../../test/fixtures/forc-projects'; +import { + DocSnippetProjectsEnum, + getDocsSnippetsForcProject, +} from '../../../test/fixtures/forc-projects'; import { defaultTxParams, getTestWallet } from '../../utils'; describe(__filename, () => { @@ -53,7 +56,13 @@ describe(__filename, () => { }); // 2. Instantiate the script main arguments - const scriptArguments = [contract.id.toB256(), assetIdA, new BN(1000), assetIdB, new BN(500)]; + const scriptArguments = [ + contract.id.toB256(), + { value: assetIdA }, + new BN(1000), + { value: assetIdB }, + new BN(500), + ]; // 3. Get the resources for inputs and outpoints const fee = request.calculateFee(gasPriceFactor); diff --git a/apps/docs-snippets/src/guide/types/asset-id.test.ts b/apps/docs-snippets/src/guide/types/asset-id.test.ts new file mode 100644 index 00000000000..ed897a8e3f3 --- /dev/null +++ b/apps/docs-snippets/src/guide/types/asset-id.test.ts @@ -0,0 +1,73 @@ +import type { AssetId, Contract, B256Address } from 'fuels'; +import { Address } from 'fuels'; + +import { DocSnippetProjectsEnum } from '../../../test/fixtures/forc-projects'; +import { createAndDeployContractFromProject } from '../../utils'; + +describe('AssetId', () => { + let contract: Contract; + const Bits256: B256Address = '0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c'; + + beforeAll(async () => { + contract = await createAndDeployContractFromProject(DocSnippetProjectsEnum.ECHO_ASSET_ID); + }); + + it('should demonstrate typed asset id example', () => { + // #region asset-id-1 + // #context import type { AssetId } from 'fuels'; + + const assetId: AssetId = { + value: Bits256, + }; + // #endregion asset-id-1 + + expect(assetId.value).toBe(Bits256); + }); + + it('should create an AssetId from a B256Address', async () => { + // #region asset-id-2 + // #context import type { AssetId } from 'fuels'; + // #context import { AssetId } from 'fuels'; + + const b256Address = '0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c'; + + const address = Address.fromB256(b256Address); + + const assetId: AssetId = address.toAssetId(); + // #endregion asset-id-2 + + const { value } = await contract.functions.echo_asset_id_comparison(assetId).simulate(); + + expect(value).toBeTruthy(); + }); + + it('should pass an asset id to a contract', async () => { + // #region asset-id-3 + // #context import type { AssetId } from 'fuels'; + + const assetId: AssetId = { + value: Bits256, + }; + + const { value } = await contract.functions.echo_asset_id_comparison(assetId).simulate(); + + expect(value).toBeTruthy(); + // #endregion asset-id-3 + }); + + it('should retrieve an asset id from a contract', async () => { + // #region asset-id-4 + // #context import type { AssetId } from 'fuels'; + + const assetId: AssetId = { + value: Bits256, + }; + + const { value } = await contract.functions.echo_asset_id().simulate(); + + expect(value).toEqual(assetId); + // #endregion asset-id-4 + + expect(value.value).toEqual(Bits256); + }); +}); diff --git a/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml b/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml index 7c6bca1cacc..ba78a81940a 100644 --- a/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml +++ b/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml @@ -19,6 +19,7 @@ members = [ "echo-employee-data-vector", "whitelisted-address-predicate", "echo-evm-address", + "echo-asset-id", "script-transfer-to-contract", "echo-bytes", "echo-raw-slice", diff --git a/apps/docs-snippets/test/fixtures/forc-projects/echo-asset-id/Forc.toml b/apps/docs-snippets/test/fixtures/forc-projects/echo-asset-id/Forc.toml new file mode 100644 index 00000000000..fb1a79e0ff7 --- /dev/null +++ b/apps/docs-snippets/test/fixtures/forc-projects/echo-asset-id/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "echo-asset-id" + +[dependencies] diff --git a/apps/docs-snippets/test/fixtures/forc-projects/echo-asset-id/src/main.sw b/apps/docs-snippets/test/fixtures/forc-projects/echo-asset-id/src/main.sw new file mode 100644 index 00000000000..2af36a25332 --- /dev/null +++ b/apps/docs-snippets/test/fixtures/forc-projects/echo-asset-id/src/main.sw @@ -0,0 +1,22 @@ +// #region asset-id-1 +contract; + +abi EvmTest { + fn echo_asset_id() -> AssetId; + fn echo_asset_id_comparison(asset_id: AssetId) -> bool; +} + +const ASSET_ID: AssetId = AssetId { + value: 0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c, +}; + +impl EvmTest for Contract { + fn echo_asset_id() -> AssetId { + ASSET_ID + } + + fn echo_asset_id_comparison(asset_id: AssetId) -> bool { + asset_id == ASSET_ID + } +} +// #endregion asset-id-1 diff --git a/apps/docs-snippets/test/fixtures/forc-projects/index.ts b/apps/docs-snippets/test/fixtures/forc-projects/index.ts index d577d6e1438..91b257ed3ea 100644 --- a/apps/docs-snippets/test/fixtures/forc-projects/index.ts +++ b/apps/docs-snippets/test/fixtures/forc-projects/index.ts @@ -23,6 +23,7 @@ export enum DocSnippetProjectsEnum { ECHO_BYTES = 'echo-bytes', ECHO_RAW_SLICE = 'echo-raw-slice', ECHO_STD_STRING = 'echo-std-string', + ECHO_ASSET_ID = 'echo-asset-id', SCRIPT_TRANSFER_TO_CONTRACT = 'script-transfer-to-contract', } diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts index 9bf9a9b0ab2..842d1bfea98 100644 --- a/apps/docs/.vitepress/config.ts +++ b/apps/docs/.vitepress/config.ts @@ -91,6 +91,10 @@ export default defineConfig({ text: 'Evm Address', link: '/guide/types/evm-address', }, + { + text: 'Asset Id', + link: '/guide/types/asset-id', + }, { text: 'Arrays', link: '/guide/types/arrays', diff --git a/apps/docs/src/guide/types/asset-id.md b/apps/docs/src/guide/types/asset-id.md new file mode 100644 index 00000000000..f3918f39c39 --- /dev/null +++ b/apps/docs/src/guide/types/asset-id.md @@ -0,0 +1,19 @@ +# Asset ID + +An Asset ID can be represented using the `AssetId` type. It's definition matches the Sway standard library type being a `Struct` wrapper around an inner `Bits256` value. + +<<< @/../../docs-snippets/src/guide/types/asset-id.test.ts#asset-id-1{ts:line-numbers} + +## Using an Asset ID + +The `AssetId` type can be integrated with your contract calls. Consider the following contract that can compares and return an Asset ID: + +<<< @/../../docs-snippets/test/fixtures/forc-projects/echo-asset-id/src/main.sw#asset-id-1{ts:line-numbers} + +The `AssetId` type can be used with the SDK and passed to the contract function as follows: + +<<< @/../../docs-snippets/src/guide/types/asset-id.test.ts#asset-id-3{ts:line-numbers} + +And to validate the returned value: + +<<< @/../../docs-snippets/src/guide/types/asset-id.test.ts#asset-id-4{ts:line-numbers} diff --git a/packages/abi-coder/src/abi-coder.ts b/packages/abi-coder/src/abi-coder.ts index f6597130e80..3cec640d574 100644 --- a/packages/abi-coder/src/abi-coder.ts +++ b/packages/abi-coder/src/abi-coder.ts @@ -3,7 +3,6 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import type { DecodedValue, InputValue, Coder } from './coders/abstract-coder'; import { ArrayCoder } from './coders/array'; -import { AssetIdCoder } from './coders/asset-id'; import { B256Coder } from './coders/b256'; import { B512Coder } from './coders/b512'; import { BooleanCoder } from './coders/boolean'; @@ -70,8 +69,6 @@ export abstract class AbiCoder { return new B256Coder(); case 'struct B512': return new B512Coder(); - case 'struct AssetId': - return new AssetIdCoder(); case BYTES_CODER_TYPE: return new ByteCoder(); case STD_STRING_CODER_TYPE: diff --git a/packages/abi-coder/src/coders/asset-id.test.ts b/packages/abi-coder/src/coders/asset-id.test.ts deleted file mode 100644 index 388165eebd2..00000000000 --- a/packages/abi-coder/src/coders/asset-id.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { AssetIdCoder } from './asset-id'; - -describe('AssetIdCoder', () => { - const B256_DECODED = '0xd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b'; - const B256_ENCODED = new Uint8Array([ - 213, 87, 156, 70, 223, 204, 127, 24, 32, 112, 19, 230, 91, 68, 228, 203, 78, 44, 34, 152, 244, - 172, 69, 123, 168, 248, 39, 67, 243, 30, 147, 11, - ]); - const B256_ZERO_DECODED = '0x0000000000000000000000000000000000000000000000000000000000000000'; - const B256_ZERO_ENCODED = new Uint8Array(32); - - const coder = new AssetIdCoder(); - - it('should encode zero as a 256 bit hash string', () => { - const expected = B256_ZERO_ENCODED; - const actual = coder.encode(B256_ZERO_DECODED); - - expect(actual).toStrictEqual(expected); - }); - - it('should encode a 256 bit hash string', () => { - const expected = B256_ENCODED; - const actual = coder.encode(B256_DECODED); - - expect(actual).toStrictEqual(expected); - }); - - it('should decode zero as a 256 bit hash string', () => { - const expectedValue = B256_ZERO_DECODED; - const expectedLength = B256_ZERO_ENCODED.length; - const [actualValue, actualLength] = coder.decode(B256_ZERO_ENCODED, 0); - - expect(actualValue).toStrictEqual(expectedValue); - expect(actualLength).toBe(expectedLength); - }); - - it('should decode a 256 bit hash string', () => { - const expectedValue = B256_DECODED; - const expectedLength = B256_ENCODED.length; - const [actualValue, actualLength] = coder.decode(B256_ENCODED, 0); - - expect(actualValue).toStrictEqual(expectedValue); - expect(actualLength).toBe(expectedLength); - }); - - it('should throw an error when encoding a 256 bit hash string that is too short', () => { - const invalidInput = B256_DECODED.slice(0, B256_DECODED.length - 1); - - expect(() => { - coder.encode(invalidInput); - }).toThrow('Invalid struct AssetId'); - }); - - it('should throw an error when decoding an encoded 256 bit hash string that is too short', () => { - const invalidInput = B256_ENCODED.slice(0, B256_ENCODED.length - 1); - - expect(() => { - coder.decode(invalidInput, 0); - }).toThrow(); - }); - - it('should throw an error when encoding a 256 bit hash string that is too long', () => { - const invalidInput = `${B256_DECODED}0`; - - expect(() => { - coder.encode(invalidInput); - }).toThrow('Invalid struct AssetId'); - }); - - it('should throw an error when encoding a 512 bit hash string', () => { - const B512 = - '0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2'; - - expect(() => { - coder.encode(B512); - }).toThrow('Invalid struct AssetId'); - }); - - it('should throw an error when decoding an encoded 256 bit hash string that is too long', () => { - const invalidInput = new Uint8Array(Array.from(Array(32).keys())); - - expect(() => { - coder.decode(invalidInput, 1); - }).toThrow('Invalid size for AssetId'); - }); - - it('should throw an error when encoding a 256 bit hash string that is not a hex string', () => { - const invalidInput = 'not a hex string'; - - expect(() => { - coder.encode(invalidInput); - }).toThrow('Invalid struct AssetId'); - }); -}); diff --git a/packages/abi-coder/src/coders/asset-id.ts b/packages/abi-coder/src/coders/asset-id.ts deleted file mode 100644 index e1b3d65d9e4..00000000000 --- a/packages/abi-coder/src/coders/asset-id.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ErrorCode } from '@fuel-ts/errors'; -import { bn, toHex } from '@fuel-ts/math'; -import { getBytes } from 'ethers'; - -import { Coder } from './abstract-coder'; - -export class AssetIdCoder extends Coder { - constructor() { - super('AssetId', 'struct AssetId', 32); - } - - encode(value: string): Uint8Array { - let encodedValue; - try { - encodedValue = getBytes(value); - } catch (error) { - this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`); - } - if (encodedValue.length !== 32) { - this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`); - } - return encodedValue; - } - - decode(data: Uint8Array, offset: number): [string, number] { - let bytes = data.slice(offset, offset + 32); - const decoded = bn(bytes); - if (decoded.isZero()) { - bytes = new Uint8Array(32); - } - if (bytes.length !== 32) { - this.throwError(ErrorCode.DECODE_ERROR, `Invalid size for AssetId.`); - } - return [toHex(bytes, 32), offset + 32]; - } -} diff --git a/packages/address/src/address.test.ts b/packages/address/src/address.test.ts index 6c2d95cdd43..d662f2130f9 100644 --- a/packages/address/src/address.test.ts +++ b/packages/address/src/address.test.ts @@ -1,6 +1,6 @@ import { FuelError } from '@fuel-ts/errors'; import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; -import type { B256AddressEvm, Bech32Address, EvmAddress } from '@fuel-ts/interfaces'; +import type { AssetId, B256AddressEvm, Bech32Address, EvmAddress } from '@fuel-ts/interfaces'; import signMessageTest from '@fuel-ts/testcases/src/signMessage.json'; import Address from './address'; @@ -342,6 +342,14 @@ describe('Address class', () => { expect(evmAddress.value).toBe(ADDRESS_B256_EVM_PADDED); }); + test('create an AssetId from B256', () => { + const address = Address.fromB256(ADDRESS_B256); + const assetId: AssetId = address.toAssetId(); + + expect(assetId).toBeDefined(); + expect(assetId.value).toBe(ADDRESS_B256); + }); + test('create an Address from an Evm Address', () => { const address = Address.fromEvmAddress(ADDRESS_EVM); diff --git a/packages/address/src/address.ts b/packages/address/src/address.ts index 4d73f3148e1..46d0d0e9f6d 100644 --- a/packages/address/src/address.ts +++ b/packages/address/src/address.ts @@ -1,6 +1,6 @@ import { FuelError } from '@fuel-ts/errors'; import { AbstractAddress } from '@fuel-ts/interfaces'; -import type { Bech32Address, B256Address, EvmAddress } from '@fuel-ts/interfaces'; +import type { Bech32Address, B256Address, EvmAddress, AssetId } from '@fuel-ts/interfaces'; import { getBytesCopy, hexlify, sha256 } from 'ethers'; import { @@ -108,6 +108,17 @@ export default class Address extends AbstractAddress { } as EvmAddress; } + /** + * Wraps the `bech32Address` property and returns as an `AssetId`. + * + * @returns The `bech32Address` property as an {@link AssetId | `AssetId`} + */ + toAssetId(): AssetId { + return { + value: this.toB256(), + } as AssetId; + } + /** * Returns the value of the `bech32Address` property * diff --git a/packages/fuel-gauge/src/multi-token-contract.test.ts b/packages/fuel-gauge/src/multi-token-contract.test.ts index f35f5339f21..22ff8b9caa8 100644 --- a/packages/fuel-gauge/src/multi-token-contract.test.ts +++ b/packages/fuel-gauge/src/multi-token-contract.test.ts @@ -72,7 +72,7 @@ describe('MultiTokenContract', () => { // define helper to get contract balance const getBalance = async (address: { value: string }, assetId: string) => { const { value } = await multiTokenContract.functions - .get_balance(address, assetId) + .get_balance(address, { value: assetId }) .simulate(); return value; }; @@ -92,7 +92,7 @@ describe('MultiTokenContract', () => { subIds.map((subId) => multiTokenContract.functions.transfer_coins_to_output( { value: userWallet.address.toB256() }, - helperDict[subId].assetId, + { value: helperDict[subId].assetId }, helperDict[subId].amount ) ) @@ -158,7 +158,7 @@ describe('MultiTokenContract', () => { // define helper to get contract balance const getBalance = async (address: { value: string }, assetId: string) => { const { value } = await multiTokenContract.functions - .get_balance(address, assetId) + .get_balance(address, { value: assetId }) .simulate(); return value; }; diff --git a/packages/fuel-gauge/src/token-test-contract.test.ts b/packages/fuel-gauge/src/token-test-contract.test.ts index ea98fe42ac8..cf9f39ddb1d 100644 --- a/packages/fuel-gauge/src/token-test-contract.test.ts +++ b/packages/fuel-gauge/src/token-test-contract.test.ts @@ -1,7 +1,7 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { generateTestWallet } from '@fuel-ts/wallet/test-utils'; -import type { BN } from 'fuels'; +import type { AssetId, BN } from 'fuels'; import { toHex, Provider, Wallet, ContractFactory, bn, BaseAssetId, FUEL_NETWORK_URL } from 'fuels'; import { FuelGaugeProjectsEnum, getFuelGaugeForcProject } from '../test/fixtures'; @@ -47,7 +47,7 @@ describe('TokenTestContract', () => { const { mintedAssets } = transactionResult; - const assetId = mintedAssets?.[0].assetId; + const assetId: AssetId = { value: mintedAssets?.[0].assetId }; const getBalance = async () => { const { value } = await token.functions.get_balance(tokenContractId, assetId).simulate(); @@ -67,7 +67,7 @@ describe('TokenTestContract', () => { // Check new wallet received the coins from the token contract const balances = await userWallet.getBalances(); - const tokenBalance = balances.find((b) => b.assetId === assetId); + const tokenBalance = balances.find((b) => b.assetId === assetId.value); expect(tokenBalance?.amount.toHex()).toEqual(toHex(50)); }); @@ -144,9 +144,9 @@ describe('TokenTestContract', () => { .txParams({ gasPrice }) .call(); const { mintedAssets } = transactionResult; - const assetId = mintedAssets?.[0].assetId || ''; + const assetId: AssetId = { value: mintedAssets?.[0].assetId || '' }; - const getBalance = async () => token.getBalance(assetId); + const getBalance = async () => token.getBalance(assetId.value); // at the start, the contract should have 100 coins expect((await getBalance()).toHex()).toEqual(bn(100).toHex()); @@ -167,7 +167,7 @@ describe('TokenTestContract', () => { const addressParameter = { value: userWallet.address, }; - const assetId = BaseAssetId; + const assetId: AssetId = { value: BaseAssetId }; await expectToThrowFuelError( () => token.functions.transfer_coins_to_output(addressParameter, assetId, 50).call(), diff --git a/packages/interfaces/src/index.ts b/packages/interfaces/src/index.ts index 07ed705d271..5aed5cd5eaa 100644 --- a/packages/interfaces/src/index.ts +++ b/packages/interfaces/src/index.ts @@ -27,6 +27,13 @@ export type EvmAddress = { value: B256AddressEvm; }; +/** + * @prop value - A wrapped 256 bit hash string + */ +export type AssetId = { + value: B256Address; +}; + export type StdString = string; /**