Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve fromEvmAddress developer experience #1309

Merged
merged 7 commits into from
Oct 6, 2023
Merged
6 changes: 6 additions & 0 deletions .changeset/two-lions-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/address": minor
"@fuel-ts/errors": minor
---

Improve developer experience of `fromEvmAddress` address helper function
2 changes: 1 addition & 1 deletion apps/docs/src/guide/types/evm-address.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ An Ethereum Virtual Machine (EVM) Address can be represented using the `EvmAddre

## 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:
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 a wrapped and Sway compatible EVM Address using the `toEvmAddress()` function:

<<< @/../../docs-snippets/src/guide/types/evm-address.test.ts#evm-address-2{ts:line-numbers}

Expand Down
77 changes: 67 additions & 10 deletions packages/address/src/address.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { FuelError } from '@fuel-ts/errors';
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
import type { Bech32Address, EvmAddress } from '@fuel-ts/interfaces';
import type { B256AddressEvm, Bech32Address, EvmAddress } from '@fuel-ts/interfaces';
import signMessageTest from '@fuel-ts/testcases/src/signMessage.json';

import Address from './address';
import * as utils from './utils';

const PUBLIC_KEY = signMessageTest.publicKey;
const ADDRESS_B256 = '0xef86afa9696cf0dc6385e2c407a6e159a1103cefb7e2ae0636fb33d3cb2a9e4a';
const ADDRESS_B256_EVM = '0x00000000000000000000000007a6e159a1103cefb7e2ae0636fb33d3cb2a9e4a';
const ADDRESS_B256_EVM_PADDED: B256AddressEvm =
'0x00000000000000000000000007a6e159a1103cefb7e2ae0636fb33d3cb2a9e4a';
const ADDRESS_EVM = '0x07a6e159a1103cefb7e2ae0636fb33d3cb2a9e4a';
const ADDRESS_BECH32: Bech32Address =
'fuel1a7r2l2tfdncdccu9utzq0fhptxs3q080kl32up3klvea8je2ne9qrqnt6n';
const ADDRESS_WORDS = [
Expand Down Expand Up @@ -128,6 +130,36 @@ describe('Address utils', () => {
expect(result).toBeTruthy();
});

test('isEvmAddress (EvmAddress)', () => {
const result = utils.isEvmAddress(ADDRESS_EVM);

expect(result).toBeTruthy();
});

test('isEvmAddress (invalid chars)', () => {
const result = utils.isEvmAddress(`${ADDRESS_EVM}/?`);

expect(result).toBeFalsy();
});

test('isEvmAddress (too long)', () => {
const result = utils.isEvmAddress(`${ADDRESS_EVM}abc12345`);

expect(result).toBeFalsy();
});

test('isEvmAddress (too short)', () => {
const result = utils.isEvmAddress('0x123');

expect(result).toBeFalsy();
});

test('isEvmAddress (no hex prefix)', () => {
danielbate marked this conversation as resolved.
Show resolved Hide resolved
const result = utils.isEvmAddress('07a6e159a1103cefb7e2ae0636fb33d3cb2a9e4a');

expect(result).toBeTruthy();
});

test('getBytesFromBech32 (bech32 to Uint8Array)', () => {
const result = utils.getBytesFromBech32(ADDRESS_BECH32);

Expand Down Expand Up @@ -166,7 +198,7 @@ describe('Address utils', () => {
test('clearFirst12BytesFromB256 (b256 to evm b256)', () => {
const result = utils.clearFirst12BytesFromB256(ADDRESS_B256);

expect(result).toEqual(ADDRESS_B256_EVM);
expect(result).toEqual(ADDRESS_B256_EVM_PADDED);
});

test('clearFirst12BytesFromB256 (invalid B256)', async () => {
Expand All @@ -177,6 +209,24 @@ describe('Address utils', () => {
);
await expectToThrowFuelError(() => utils.clearFirst12BytesFromB256(invalidB256), expectedError);
});

test('padFirst12BytesOfEvmAddress (evm Address to b256)', () => {
const result = utils.padFirst12BytesOfEvmAddress(ADDRESS_EVM);

expect(result).toEqual(ADDRESS_B256_EVM_PADDED);
});

test('padFirst12BytesOfEvmAddress (invalid EVM Address)', async () => {
const invalidEvmAddress = '0x123';
const expectedError = new FuelError(
FuelError.CODES.INVALID_EVM_ADDRESS,
'Invalid EVM address format.'
);
await expectToThrowFuelError(
() => utils.padFirst12BytesOfEvmAddress(invalidEvmAddress),
expectedError
);
});
});

describe('Address class', () => {
Expand Down Expand Up @@ -243,6 +293,12 @@ describe('Address class', () => {
expect(address.toB256()).toEqual(signMessageTest.b256Address);
});

test('create an Address class fromDynamicInput [evmAddress]', () => {
const address = Address.fromDynamicInput(ADDRESS_EVM);

expect(address.toB256()).toEqual(ADDRESS_B256_EVM_PADDED);
});

test('create an Address class fromDynamicInput [bad input]', async () => {
const expectedError = new FuelError(
FuelError.CODES.PARSE_FAILED,
Expand All @@ -265,16 +321,17 @@ describe('Address class', () => {
const evmAddress: EvmAddress = address.toEvmAddress();

expect(evmAddress).toBeDefined();
expect(evmAddress.value).toBe(ADDRESS_B256_EVM);
expect(evmAddress.value).toBe(ADDRESS_B256_EVM_PADDED);
});

test('create an Address class fromEvmAddress', () => {
const evmAddress: EvmAddress = {
value: ADDRESS_B256_EVM,
};
test('create an Address from an Evm Address', () => {
const address = Address.fromEvmAddress(ADDRESS_EVM);

const address = Address.fromEvmAddress(evmAddress);
const evmAddressWrapped: EvmAddress = {
value: ADDRESS_B256_EVM_PADDED,
};

expect(address.toB256()).toEqual(ADDRESS_B256_EVM);
expect(address.toEvmAddress()).toMatchObject(evmAddressWrapped);
expect(address.toB256()).toEqual(ADDRESS_B256_EVM_PADDED);
});
});
14 changes: 11 additions & 3 deletions packages/address/src/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
isPublicKey,
isB256,
clearFirst12BytesFromB256,
isEvmAddress,
padFirst12BytesOfEvmAddress,
} from './utils';

/**
Expand Down Expand Up @@ -200,18 +202,24 @@ export default class Address extends AbstractAddress {
return Address.fromB256(address);
}

if (isEvmAddress(address)) {
return Address.fromEvmAddress(address);
}

throw new FuelError(
FuelError.CODES.PARSE_FAILED,
`Unknown address format: only 'Bech32', 'B256', or 'Public Key (512)' are supported.`
);
}

/**
* Takes an `EvmAddress` and returns back an `Address`
* Takes an Evm Address and returns back an `Address`
*
* @returns A new `Address` instance
*/
static fromEvmAddress(evmAddress: EvmAddress): Address {
return new Address(toBech32(evmAddress.value));
static fromEvmAddress(evmAddress: string): Address {
const paddedAddress = padFirst12BytesOfEvmAddress(evmAddress);

return new Address(toBech32(paddedAddress));
}
}
27 changes: 27 additions & 0 deletions packages/address/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ export function isPublicKey(address: string): boolean {
return (address.length === 130 || address.length === 128) && /(0x)?[0-9a-f]{128}$/i.test(address);
}

/**
* Determines if a given string is in EVM Address format
*
* @hidden
*/
export function isEvmAddress(address: string): boolean {
return (address.length === 42 || address.length === 40) && /(0x)?[0-9a-f]{40}$/i.test(address);
danielbate marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Takes a Bech32 address and returns the byte data
*
Expand Down Expand Up @@ -163,3 +172,21 @@ export const clearFirst12BytesFromB256 = (b256: B256Address): B256AddressEvm =>

return bytes;
};

/**
* Pads the first 12 bytes of an Evm address. This is useful for padding addresses returned from
* the EVM to interact with the Sway EVM Address Type.
*
* @param address - Evm address to be padded
* @returns Evm address padded to a b256 address
*
* @hidden
*/
export const padFirst12BytesOfEvmAddress = (address: string): B256AddressEvm => {
if (!isEvmAddress(address)) {
throw new FuelError(FuelError.CODES.INVALID_EVM_ADDRESS, 'Invalid EVM address format.');
}

const prefixedAddress = address.startsWith('0x') ? address : `0x${address}`;
return prefixedAddress.replace('0x', '0x000000000000000000000000') as B256AddressEvm;
};
1 change: 1 addition & 0 deletions packages/errors/src/error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum ErrorCode {

// address
INVALID_BECH32_ADDRESS = 'invalid-bech32-address',
INVALID_EVM_ADDRESS = 'invalid-evm-address',

// provider
INVALID_URL = 'invalid-url',
Expand Down