From ac9362c2fa1ed32889a168b83be5d96810680d80 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Mon, 29 May 2023 12:11:15 +0100 Subject: [PATCH] feat: support `EvmAddress` (#995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: create evm address interface * feat: create evm address clas * feat: unit tests for evm address wrapper class * chore: linting * chore: linting * feat: support from public key in evm address * feat: remove evm address class and unit tests * feat: create types for B256 Evm and Evm address * feat: create address utility function to convert a b256 to an evm b256 * feat: add address function to return as evm addres * feat: add unit tests for address to evm address and utility function * feat: create snippets util function generate test wallet and deploy a contract * test: create docs snippets tests for evm address * chore: remove redundant gitignore from evm address doc snippet contract * test: rename evm address docs snippet test * docs: create documentation for evm address * docs: modify docs snippets for evm address * docs: add evm address to docs config * test: alter evm address test asserttions * docs: update evm address title capitalisation * feat: add evm address type to typegen * test: add unit test for evm address type with forc project * test: add evm address to full typegen test suit * feat: add ignore evm struct to struct typegen * feat: add evm address type to supported types in typegen * chore: changeset * feat: add forc projects to workspace * test: add address import to dts test * Update packages/abi-typegen/test/fixtures/forc-projects/evm-address/Forc.toml Co-authored-by: Sérgio Torres <30977845+Torres-ssf@users.noreply.github.com> * feat: remove redundant gitignore from evm address cargo * feat: remove address import for evm address typegen * chore: force rebuild * feat: DRY evm address contract in docs snippets * refactor: DRY EVM Address sway contract in snippets * test: remove address import assert from typegen evm test * feat: support from evm address in address --------- Co-authored-by: danielbate <--global> Co-authored-by: Sérgio Torres <30977845+Torres-ssf@users.noreply.github.com> --- .changeset/shiny-socks-juggle.md | 7 ++ apps/docs-snippets/projects/Forc.toml | 1 + .../projects/echo-evm-address/Forc.toml | 7 ++ .../projects/echo-evm-address/src/main.sw | 26 +++++++ apps/docs-snippets/projects/index.ts | 1 + .../src/guide/types/evm-address.test.ts | 74 +++++++++++++++++++ apps/docs-snippets/src/utils.ts | 17 ++++- apps/docs/.vitepress/config.ts | 4 + apps/docs/src/guide/types/evm-address.md | 25 +++++++ .../src/abi/types/EvmAddressType.test.ts | 36 +++++++++ .../src/abi/types/EvmAddressType.ts | 28 +++++++ .../src/abi/types/StructType.test.ts | 3 + .../abi-typegen/src/abi/types/StructType.ts | 2 +- .../src/utils/supportedTypes.test.ts | 2 +- .../abi-typegen/src/utils/supportedTypes.ts | 2 + .../test/fixtures/forc-projects/Forc.toml | 1 + .../forc-projects/evm-address/Forc.toml | 7 ++ .../forc-projects/evm-address/src/main.sw | 13 ++++ .../fixtures/forc-projects/full/src/main.sw | 4 + .../test/fixtures/forc-projects/index.ts | 1 + .../test/fixtures/templates/contract/dts.hbs | 5 ++ packages/address/src/address.test.ts | 35 ++++++++- packages/address/src/address.ts | 23 +++++- packages/address/src/utils.ts | 24 ++++++ packages/interfaces/src/index.ts | 6 ++ 25 files changed, 349 insertions(+), 5 deletions(-) create mode 100644 .changeset/shiny-socks-juggle.md create mode 100644 apps/docs-snippets/projects/echo-evm-address/Forc.toml create mode 100644 apps/docs-snippets/projects/echo-evm-address/src/main.sw create mode 100644 apps/docs-snippets/src/guide/types/evm-address.test.ts create mode 100644 apps/docs/src/guide/types/evm-address.md create mode 100644 packages/abi-typegen/src/abi/types/EvmAddressType.test.ts create mode 100644 packages/abi-typegen/src/abi/types/EvmAddressType.ts create mode 100644 packages/abi-typegen/test/fixtures/forc-projects/evm-address/Forc.toml create mode 100644 packages/abi-typegen/test/fixtures/forc-projects/evm-address/src/main.sw diff --git a/.changeset/shiny-socks-juggle.md b/.changeset/shiny-socks-juggle.md new file mode 100644 index 00000000000..f9877a41c0c --- /dev/null +++ b/.changeset/shiny-socks-juggle.md @@ -0,0 +1,7 @@ +--- +"@fuel-ts/abi-typegen": patch +"@fuel-ts/address": patch +"@fuel-ts/interfaces": patch +--- + +Support EVM Address type diff --git a/apps/docs-snippets/projects/Forc.toml b/apps/docs-snippets/projects/Forc.toml index 79742a82cae..882a829c500 100644 --- a/apps/docs-snippets/projects/Forc.toml +++ b/apps/docs-snippets/projects/Forc.toml @@ -18,4 +18,5 @@ members = [ "return-true-predicate", "echo-employee-data-vector", "whitelisted-address-predicate", + "echo-evm-address", ] diff --git a/apps/docs-snippets/projects/echo-evm-address/Forc.toml b/apps/docs-snippets/projects/echo-evm-address/Forc.toml new file mode 100644 index 00000000000..f7202e72985 --- /dev/null +++ b/apps/docs-snippets/projects/echo-evm-address/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["FuelLabs"] +entry = "main.sw" +license = "Apache-2.0" +name = "echo-evm-address" + +[dependencies] diff --git a/apps/docs-snippets/projects/echo-evm-address/src/main.sw b/apps/docs-snippets/projects/echo-evm-address/src/main.sw new file mode 100644 index 00000000000..9650c4d6eb6 --- /dev/null +++ b/apps/docs-snippets/projects/echo-evm-address/src/main.sw @@ -0,0 +1,26 @@ +// #region evm-address-1 +contract; + +use std::vm::evm::evm_address::EvmAddress; + +configurable { + B256_ADDR: b256 = 0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6, +} + +abi EvmTest { + fn echo_address() -> EvmAddress; + fn echo_address_comparison(evm_addr: EvmAddress) -> bool; +} + +impl EvmTest for Contract { + fn echo_address() -> EvmAddress { + return EvmAddress::from(B256_ADDR); + } + + fn echo_address_comparison(evm_addr: EvmAddress) -> bool { + let evm_addr2 = EvmAddress::from(B256_ADDR); + + evm_addr == evm_addr2 + } +} +// #endregion evm-address-1 diff --git a/apps/docs-snippets/projects/index.ts b/apps/docs-snippets/projects/index.ts index c90642cbd89..f1469f93e65 100644 --- a/apps/docs-snippets/projects/index.ts +++ b/apps/docs-snippets/projects/index.ts @@ -19,6 +19,7 @@ export enum SnippetProjectEnum { RETURN_TRUE_PREDICATE = 'return-true-predicate', ECHO_EMPLOYEE_DATA_VECTOR = 'echo-employee-data-vector', WHITELISTED_ADDRESS_PREDICATE = 'whitelisted-address-predicate', + ECHO_EVM_ADDRESS = 'echo-evm-address', } export const getSnippetProjectArtifacts = (project: SnippetProjectEnum) => diff --git a/apps/docs-snippets/src/guide/types/evm-address.test.ts b/apps/docs-snippets/src/guide/types/evm-address.test.ts new file mode 100644 index 00000000000..f3f9f28e7df --- /dev/null +++ b/apps/docs-snippets/src/guide/types/evm-address.test.ts @@ -0,0 +1,74 @@ +import type { B256AddressEvm, Contract, EvmAddress } from 'fuels'; +import { Address } from 'fuels'; + +import { SnippetProjectEnum } from '../../../projects'; +import { createAndDeployContractFromProject } from '../../utils'; + +describe('EvMAddress', () => { + let contract: Contract; + const Bits256: B256AddressEvm = + '0x000000000000000000000000210cf886ce41952316441ae4cac35f00f0e882a6'; + + beforeAll(async () => { + contract = await createAndDeployContractFromProject(SnippetProjectEnum.ECHO_EVM_ADDRESS); + }); + + it('should demonstrate typed evm address example', () => { + // #region evm-address-1 + // #context import type { EvmAddress } from 'fuels'; + + const evmAddress: EvmAddress = { + value: Bits256, + }; + // #endregion evm-address-1 + + expect(evmAddress.value).toBe(Bits256); + }); + + it('should create an Evm Address from a B256Address', async () => { + // #region evm-address-2 + // #context import type { EvmAddress } from 'fuels'; + // #context import { Address } from 'fuels'; + + const b256Address = '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6'; + + const address = Address.fromB256(b256Address); + + const evmAddress: EvmAddress = address.toEvmAddress(); + // #endregion evm-address-2 + + const { value } = await contract.functions.echo_address_comparison(evmAddress).get(); + + expect(value).toBeTruthy(); + }); + + it('should pass an evm address to a contract', async () => { + // #region evm-address-3 + // #context import type { EvmAddress } from 'fuels'; + + const evmAddress: EvmAddress = { + value: Bits256, + }; + + const { value } = await contract.functions.echo_address_comparison(evmAddress).get(); + + expect(value).toBeTruthy(); + // #endregion evm-address-3 + }); + + it('should retrieve an evm address from a contract', async () => { + // #region evm-address-4 + // #context import type { EvmAddress } from 'fuels'; + + const evmAddress: EvmAddress = { + value: Bits256, + }; + + const { value } = await contract.functions.echo_address().get(); + + expect(value).toEqual(evmAddress); + // #endregion evm-address-4 + + expect(value.value).toEqual(Bits256); + }); +}); diff --git a/apps/docs-snippets/src/utils.ts b/apps/docs-snippets/src/utils.ts index 7af5c901f6f..28158a201dd 100644 --- a/apps/docs-snippets/src/utils.ts +++ b/apps/docs-snippets/src/utils.ts @@ -1,4 +1,4 @@ -import type { CoinQuantityLike } from 'fuels'; +import type { CoinQuantityLike, Contract } from 'fuels'; import { FUEL_NETWORK_URL, NativeAssetId, @@ -7,8 +7,12 @@ import { Wallet, WalletUnlocked, coinQuantityfy, + ContractFactory, } from 'fuels'; +import type { SnippetProjectEnum } from '../projects'; +import { getSnippetProjectArtifacts } from '../projects'; + export const getTestWallet = async () => { // create a provider using the Fuel network URL const provider = new Provider(FUEL_NETWORK_URL); @@ -53,3 +57,14 @@ export const getTestWallet = async () => { // return the test wallet return testWallet; }; + +export const createAndDeployContractFromProject = async ( + project: SnippetProjectEnum +): Promise => { + const wallet = await getTestWallet(); + const { abiContents, binHelixfied } = getSnippetProjectArtifacts(project); + + const contractFactory = new ContractFactory(binHelixfied, abiContents, wallet); + + return contractFactory.deployContract(); +}; diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts index 0e6646b76c2..53672446d7a 100644 --- a/apps/docs/.vitepress/config.ts +++ b/apps/docs/.vitepress/config.ts @@ -63,6 +63,10 @@ export default defineConfig({ text: 'Address', link: '/guide/types/address', }, + { + text: 'Evm Address', + link: '/guide/types/evm-address', + }, { text: 'Arrays', link: '/guide/types/arrays', diff --git a/apps/docs/src/guide/types/evm-address.md b/apps/docs/src/guide/types/evm-address.md new file mode 100644 index 00000000000..b59ed6ca3c0 --- /dev/null +++ b/apps/docs/src/guide/types/evm-address.md @@ -0,0 +1,25 @@ +# EvmAddress + +An Ethereum Virtual Machine (EVM) Address can be represented using the `EvmAddress` type. It's definition matches the Sway standard library type being a `Struct` wrapper around an inner `Bits256` value. + +<<< @/../../docs-snippets/src/guide/types/evm-address.test.ts#evm-address-1{ts:line-numbers} + +## Creating an EVM Address + +An EVM Address only has 20 bytes therefore the first 12 bytes of the `Bits256` value are set to 0. Within the SDK, an `Address` can be instantiated and converted to an EVM Address using the `toEvmAddress()` function: + +<<< @/../../docs-snippets/src/guide/types/evm-address.test.ts#evm-address-2{ts:line-numbers} + +## Using an EVM Address + +The `EvmAddress` type can be integrated with your contract calls. Consider the following contract that can compare and return an EVM Address: + +<<< @/../../docs-snippets/projects/echo-evm-address/src/main.sw#evm-address-1{ts:line-numbers} + +The `EvmAddress` type can be used with the SDK and passed to the contract function as follows: + +<<< @/../../docs-snippets/src/guide/types/evm-address.test.ts#evm-address-3{ts:line-numbers} + +And to validate the returned value: + +<<< @/../../docs-snippets/src/guide/types/evm-address.test.ts#evm-address-4{ts:line-numbers} \ No newline at end of file diff --git a/packages/abi-typegen/src/abi/types/EvmAddressType.test.ts b/packages/abi-typegen/src/abi/types/EvmAddressType.test.ts new file mode 100644 index 00000000000..07f3f11ec75 --- /dev/null +++ b/packages/abi-typegen/src/abi/types/EvmAddressType.test.ts @@ -0,0 +1,36 @@ +import { getProjectResources, ForcProjectsEnum } from '../../../test/fixtures/forc-projects/index'; +import type { IRawAbiTypeRoot } from '../../index'; +import { findType } from '../../utils/findType'; +import { makeType } from '../../utils/makeType'; +import * as parseTypeArgumentsMod from '../../utils/parseTypeArguments'; + +import { EvmAddressType } from './EvmAddressType'; +import { StructType } from './StructType'; +import { VectorType } from './VectorType'; + +describe('EvmAddressType.ts', () => { + test('should properly parse type attributes', () => { + const parseTypeArguments = jest.spyOn(parseTypeArgumentsMod, 'parseTypeArguments'); + + const project = getProjectResources(ForcProjectsEnum.EVM_ADDRESS); + + const rawTypes = project.abiContents.types; + const types = rawTypes.map((rawAbiType: IRawAbiTypeRoot) => makeType({ rawAbiType })); + + const suitableForEvmAddress = EvmAddressType.isSuitableFor({ type: EvmAddressType.swayType }); + const suitableForStruct = EvmAddressType.isSuitableFor({ type: StructType.swayType }); + const suitableForVector = EvmAddressType.isSuitableFor({ type: VectorType.swayType }); + + expect(suitableForEvmAddress).toEqual(true); + expect(suitableForStruct).toEqual(false); + expect(suitableForVector).toEqual(false); + + parseTypeArguments.mockClear(); + + const evmAddress = findType({ types, typeId: 1 }) as EvmAddressType; + + expect(evmAddress.attributes.inputLabel).toEqual('EvmAddress'); + expect(evmAddress.attributes.outputLabel).toEqual('EvmAddress'); + expect(evmAddress.requiredFuelsMembersImports).toStrictEqual(['EvmAddress']); + }); +}); diff --git a/packages/abi-typegen/src/abi/types/EvmAddressType.ts b/packages/abi-typegen/src/abi/types/EvmAddressType.ts new file mode 100644 index 00000000000..e1513d9440f --- /dev/null +++ b/packages/abi-typegen/src/abi/types/EvmAddressType.ts @@ -0,0 +1,28 @@ +import type { IType } from '../../types/interfaces/IType'; + +import { AType } from './AType'; + +export class EvmAddressType extends AType implements IType { + public static swayType = 'struct EvmAddress'; + + public name = 'evmAddress'; + + static MATCH_REGEX: RegExp = /^struct EvmAddress$/m; + + static isSuitableFor(params: { type: string }) { + return EvmAddressType.MATCH_REGEX.test(params.type); + } + + public parseComponentsAttributes(_params: { types: IType[] }) { + const capitalizedName = 'EvmAddress'; + + this.attributes = { + inputLabel: capitalizedName, + outputLabel: capitalizedName, + }; + + this.requiredFuelsMembersImports = [capitalizedName]; + + return this.attributes; + } +} diff --git a/packages/abi-typegen/src/abi/types/StructType.test.ts b/packages/abi-typegen/src/abi/types/StructType.test.ts index 422644cade7..aae433b7472 100644 --- a/packages/abi-typegen/src/abi/types/StructType.test.ts +++ b/packages/abi-typegen/src/abi/types/StructType.test.ts @@ -5,6 +5,7 @@ import { findType } from '../../utils/findType'; import { makeType } from '../../utils/makeType'; import * as parseTypeArgumentsMod from '../../utils/parseTypeArguments'; +import { EvmAddressType } from './EvmAddressType'; import { StructType } from './StructType'; import { U16Type } from './U16Type'; @@ -19,9 +20,11 @@ describe('StructType.ts', () => { const suitableForStruct = StructType.isSuitableFor({ type: StructType.swayType }); const suitableForU16 = StructType.isSuitableFor({ type: U16Type.swayType }); + const suitableForEvmAddress = StructType.isSuitableFor({ type: EvmAddressType.swayType }); expect(suitableForStruct).toEqual(true); expect(suitableForU16).toEqual(false); + expect(suitableForEvmAddress).toEqual(false); // validating `struct C`, with nested `typeArguments` parseTypeArguments.mockClear(); diff --git a/packages/abi-typegen/src/abi/types/StructType.ts b/packages/abi-typegen/src/abi/types/StructType.ts index 5426e2e6a87..f1d4bd46b9f 100644 --- a/packages/abi-typegen/src/abi/types/StructType.ts +++ b/packages/abi-typegen/src/abi/types/StructType.ts @@ -13,7 +13,7 @@ export class StructType extends AType implements IType { public name = 'struct'; static MATCH_REGEX: RegExp = /^struct (.+)$/m; - static IGNORE_REGEX: RegExp = /^struct (Vec|RawVec)$/m; + static IGNORE_REGEX: RegExp = /^struct (Vec|RawVec|EvmAddress)$/m; static isSuitableFor(params: { type: string }) { const isAMatch = StructType.MATCH_REGEX.test(params.type); diff --git a/packages/abi-typegen/src/utils/supportedTypes.test.ts b/packages/abi-typegen/src/utils/supportedTypes.test.ts index e2103b289c6..33f195baa6f 100644 --- a/packages/abi-typegen/src/utils/supportedTypes.test.ts +++ b/packages/abi-typegen/src/utils/supportedTypes.test.ts @@ -2,6 +2,6 @@ import { supportedTypes } from './supportedTypes'; describe('supportedTypes.ts', () => { test('should export all supported types', () => { - expect(supportedTypes.length).toEqual(16); + expect(supportedTypes.length).toEqual(17); }); }); diff --git a/packages/abi-typegen/src/utils/supportedTypes.ts b/packages/abi-typegen/src/utils/supportedTypes.ts index faa8e435337..15a664b94f0 100644 --- a/packages/abi-typegen/src/utils/supportedTypes.ts +++ b/packages/abi-typegen/src/utils/supportedTypes.ts @@ -3,6 +3,7 @@ import { B256Type } from '../abi/types/B256Type'; import { B512Type } from '../abi/types/B512Type'; import { BoolType } from '../abi/types/BoolType'; import { EnumType } from '../abi/types/EnumType'; +import { EvmAddressType } from '../abi/types/EvmAddressType'; import { GenericType } from '../abi/types/GenericType'; import { OptionType } from '../abi/types/OptionType'; import { RawUntypedPtr } from '../abi/types/RawUntypedPtr'; @@ -32,4 +33,5 @@ export const supportedTypes = [ U64Type, U8Type, VectorType, + EvmAddressType, ]; diff --git a/packages/abi-typegen/test/fixtures/forc-projects/Forc.toml b/packages/abi-typegen/test/fixtures/forc-projects/Forc.toml index 3a9d9e542a1..93b2641f3b4 100644 --- a/packages/abi-typegen/test/fixtures/forc-projects/Forc.toml +++ b/packages/abi-typegen/test/fixtures/forc-projects/Forc.toml @@ -6,6 +6,7 @@ members = [ "./enum-of-structs", "./enum-simple", "./enum-simple-native", + "./evm-address", "./fn-void", "./full", "./minimal", diff --git a/packages/abi-typegen/test/fixtures/forc-projects/evm-address/Forc.toml b/packages/abi-typegen/test/fixtures/forc-projects/evm-address/Forc.toml new file mode 100644 index 00000000000..6159a10a904 --- /dev/null +++ b/packages/abi-typegen/test/fixtures/forc-projects/evm-address/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["FuelLabs"] +entry = "main.sw" +license = "Apache-2.0" +name = "evm-address" + +[dependencies] diff --git a/packages/abi-typegen/test/fixtures/forc-projects/evm-address/src/main.sw b/packages/abi-typegen/test/fixtures/forc-projects/evm-address/src/main.sw new file mode 100644 index 00000000000..709833db37c --- /dev/null +++ b/packages/abi-typegen/test/fixtures/forc-projects/evm-address/src/main.sw @@ -0,0 +1,13 @@ +contract; + +use std::vm::evm::evm_address::EvmAddress; + +abi EvmAddressTest { + fn main(raw_address: b256) -> EvmAddress; +} + +impl EvmAddressTest for Contract { + fn main(raw_address: b256) -> EvmAddress { + EvmAddress::from(raw_address) + } +} diff --git a/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw b/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw index c93d72dfed6..73f904692d9 100644 --- a/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw +++ b/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw @@ -1,5 +1,7 @@ contract; +use std::vm::evm::evm_address::EvmAddress; + enum MyEnum { Checked: (), Pending: () @@ -36,6 +38,7 @@ abi MyContract { fn types_vector_option(x: Vec) -> Vec; fn types_option(x: Option) -> Option; fn types_option_geo(x: Option) -> Option; + fn types_evm_address(x: EvmAddress) -> EvmAddress; } impl MyContract for Contract { @@ -55,4 +58,5 @@ impl MyContract for Contract { fn types_vector_option(x: Vec) -> Vec { x } fn types_option(x: Option) -> Option { x } fn types_option_geo(x: Option) -> Option { x } + fn types_evm_address(x: EvmAddress) -> EvmAddress { EvmAddress::from(0x0606060606060606060606060606060606060606060606060606060606060606) } } diff --git a/packages/abi-typegen/test/fixtures/forc-projects/index.ts b/packages/abi-typegen/test/fixtures/forc-projects/index.ts index 68041ca72e3..8e94d3aab5d 100644 --- a/packages/abi-typegen/test/fixtures/forc-projects/index.ts +++ b/packages/abi-typegen/test/fixtures/forc-projects/index.ts @@ -10,6 +10,7 @@ export enum ForcProjectsEnum { ENUM_OF_STRUCTS = 'enum-of-structs', ENUM_SIMPLE = 'enum-simple', ENUM_SIMPLE_NATIVE = 'enum-simple-native', + EVM_ADDRESS = 'evm-address', FN_VOID = 'fn-void', FULL = 'full', MINIMAL = 'minimal', diff --git a/packages/abi-typegen/test/fixtures/templates/contract/dts.hbs b/packages/abi-typegen/test/fixtures/templates/contract/dts.hbs index 533e4122065..1648b6929c6 100644 --- a/packages/abi-typegen/test/fixtures/templates/contract/dts.hbs +++ b/packages/abi-typegen/test/fixtures/templates/contract/dts.hbs @@ -15,6 +15,7 @@ import type { BytesLike, Contract, DecodedValue, + EvmAddress, FunctionFragment, Interface, InvokeFunction, @@ -36,6 +37,7 @@ interface MyContractAbiInterface extends Interface { types_b256: FunctionFragment; types_bool: FunctionFragment; types_enum: FunctionFragment; + types_evm_address: FunctionFragment; types_option: FunctionFragment; types_option_geo: FunctionFragment; types_str: FunctionFragment; @@ -54,6 +56,7 @@ interface MyContractAbiInterface extends Interface { encodeFunctionData(functionFragment: 'types_b256', values: [string]): Uint8Array; encodeFunctionData(functionFragment: 'types_bool', values: [boolean]): Uint8Array; encodeFunctionData(functionFragment: 'types_enum', values: [MyEnumInput]): Uint8Array; + encodeFunctionData(functionFragment: 'types_evm_address', values: [EvmAddress]): Uint8Array; encodeFunctionData(functionFragment: 'types_option', values: [Option]): Uint8Array; encodeFunctionData(functionFragment: 'types_option_geo', values: [Option]): Uint8Array; encodeFunctionData(functionFragment: 'types_str', values: [string]): Uint8Array; @@ -71,6 +74,7 @@ interface MyContractAbiInterface extends Interface { decodeFunctionData(functionFragment: 'types_b256', data: BytesLike): DecodedValue; decodeFunctionData(functionFragment: 'types_bool', data: BytesLike): DecodedValue; decodeFunctionData(functionFragment: 'types_enum', data: BytesLike): DecodedValue; + decodeFunctionData(functionFragment: 'types_evm_address', data: BytesLike): DecodedValue; decodeFunctionData(functionFragment: 'types_option', data: BytesLike): DecodedValue; decodeFunctionData(functionFragment: 'types_option_geo', data: BytesLike): DecodedValue; decodeFunctionData(functionFragment: 'types_str', data: BytesLike): DecodedValue; @@ -92,6 +96,7 @@ export class MyContractAbi extends Contract { types_b256: InvokeFunction<[x: string], string>; types_bool: InvokeFunction<[x: boolean], boolean>; types_enum: InvokeFunction<[x: MyEnumInput], MyEnumOutput>; + types_evm_address: InvokeFunction<[x: EvmAddress], EvmAddress>; types_option: InvokeFunction<[x: Option], Option>; types_option_geo: InvokeFunction<[x: Option], Option>; types_str: InvokeFunction<[x: string], string>; diff --git a/packages/address/src/address.test.ts b/packages/address/src/address.test.ts index 94d99af572f..cf197682e8d 100644 --- a/packages/address/src/address.test.ts +++ b/packages/address/src/address.test.ts @@ -1,4 +1,4 @@ -import type { Bech32Address } from '@fuel-ts/interfaces'; +import type { Bech32Address, EvmAddress } from '@fuel-ts/interfaces'; import signMessageTest from '@fuel-ts/testcases/src/signMessage.json'; import Address from './address'; @@ -6,6 +6,7 @@ import * as utils from './utils'; const PUBLIC_KEY = signMessageTest.publicKey; const ADDRESS_B256 = '0xef86afa9696cf0dc6385e2c407a6e159a1103cefb7e2ae0636fb33d3cb2a9e4a'; +const ADDRESS_B256_EVM = '0x00000000000000000000000007a6e159a1103cefb7e2ae0636fb33d3cb2a9e4a'; const ADDRESS_BECH32: Bech32Address = 'fuel1a7r2l2tfdncdccu9utzq0fhptxs3q080kl32up3klvea8je2ne9qrqnt6n'; const ADDRESS_WORDS = [ @@ -154,6 +155,20 @@ describe('Address utils', () => { expect(finalResult).toEqual(ADDRESS); }); + + test('clearFirst12BytesFromB256 (b256 to evm b256)', () => { + const result = utils.clearFirst12BytesFromB256(ADDRESS_B256); + + expect(result).toEqual(ADDRESS_B256_EVM); + }); + + test('clearFirst12BytesFromB256 (invalid B256)', () => { + const invalidB256 = '0x123'; + + expect(() => utils.clearFirst12BytesFromB256(invalidB256)).toThrow( + `Cannot generate EVM Address B256 from B256: ${invalidB256}` + ); + }); }); describe('Address class', () => { @@ -234,4 +249,22 @@ describe('Address class', () => { expect(address).toBe(address); expect(newAddress).not.toBe(address); }); + + test('create an EvmAddress from B256', () => { + const address = Address.fromB256(ADDRESS_B256); + const evmAddress: EvmAddress = address.toEvmAddress(); + + expect(evmAddress).toBeDefined(); + expect(evmAddress.value).toBe(ADDRESS_B256_EVM); + }); + + test('create an Address class fromEvmAddress', () => { + const evmAddress: EvmAddress = { + value: ADDRESS_B256_EVM, + }; + + const address = Address.fromEvmAddress(evmAddress); + + expect(address.toB256()).toEqual(ADDRESS_B256_EVM); + }); }); diff --git a/packages/address/src/address.ts b/packages/address/src/address.ts index 26974c15b92..e17505bb6c9 100644 --- a/packages/address/src/address.ts +++ b/packages/address/src/address.ts @@ -1,7 +1,7 @@ import { Logger } from '@ethersproject/logger'; import { sha256 } from '@ethersproject/sha2'; import { AbstractAddress } from '@fuel-ts/interfaces'; -import type { Bech32Address, B256Address } from '@fuel-ts/interfaces'; +import type { Bech32Address, B256Address, EvmAddress } from '@fuel-ts/interfaces'; import { versions } from '@fuel-ts/versions'; import { @@ -13,6 +13,7 @@ import { getRandomB256, isPublicKey, isB256, + clearFirst12BytesFromB256, } from './utils'; const logger = new Logger(versions.FUELS); @@ -76,6 +77,18 @@ export default class Address extends AbstractAddress { return this.toString(); } + /** + * Returns the address value as an EvmAddress + * @returns the bech32 address as an EvmAddress + */ + toEvmAddress(): EvmAddress { + const b256Address = toB256(this.bech32Address); + + return { + value: clearFirst12BytesFromB256(b256Address), + } as EvmAddress; + } + /** * Returns the value of this Address value * @returns a string address in Bech32m Format @@ -165,4 +178,12 @@ export default class Address extends AbstractAddress { throw new Error('Unknown address format: only Bech32, B256, or Public Key (512) supported'); } + + /** + * Takes an EvmAddress and returns back an Address + * @returns a new `Address` instance + */ + static fromEvmAddress(evmAddress: EvmAddress): Address { + return new Address(toBech32(evmAddress.value)); + } } diff --git a/packages/address/src/utils.ts b/packages/address/src/utils.ts index ce54f0de503..cbe8d3232a3 100644 --- a/packages/address/src/utils.ts +++ b/packages/address/src/utils.ts @@ -8,6 +8,7 @@ import type { AddressLike, ContractIdLike, AbstractAddress, + B256AddressEvm, } from '@fuel-ts/interfaces'; import { randomBytes } from '@fuel-ts/keystore'; import { versions } from '@fuel-ts/versions'; @@ -103,3 +104,26 @@ export const addressify = (addressLike: AddressLike | ContractIdLike): AbstractA }; export const getRandomB256 = () => hexlify(randomBytes(32)); + +/** + * Takes a B256 address and clears the first 12 bytes, this is required for an EVM Address + * + * @param b256 - the address to clear + * @returns b256 with first 12 bytes cleared + */ +export const clearFirst12BytesFromB256 = (b256: B256Address): B256AddressEvm => { + let bytes; + + try { + if (!isB256(b256)) { + throw new Error(); + } + + bytes = getBytesFromBech32(toBech32(b256)); + bytes = hexlify(bytes.fill(0, 0, 12)) as B256AddressEvm; + } catch (error) { + throw new Error(`Cannot generate EVM Address B256 from B256: ${b256}`); + } + + return bytes; +}; diff --git a/packages/interfaces/src/index.ts b/packages/interfaces/src/index.ts index 4f08846d692..6a1ab3957e2 100644 --- a/packages/interfaces/src/index.ts +++ b/packages/interfaces/src/index.ts @@ -5,6 +5,12 @@ export type Bech32Address = `fuel${string}`; // #endregion bech32-1 export type B256Address = string; +export type B256AddressEvm = `0x000000000000000000000000${string}`; + +export type EvmAddress = { + value: B256AddressEvm; +}; + export abstract class AbstractScriptRequest { abstract bytes: Uint8Array; abstract encodeScriptData: (data: T) => Uint8Array;