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: accepting addresses as string #1583

Merged
merged 13 commits into from
Dec 22, 2023
24 changes: 24 additions & 0 deletions packages/address/src/address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,4 +376,28 @@ describe('Address class', () => {
);
await expectToThrowFuelError(() => Address.fromEvmAddress(address), expectedError);
});

describe('fromAddressOrBech32String', () => {
it('should handle bech-32 string address', () => {
const result = Address.fromAddressOrBech32String(ADDRESS_BECH32);
expect(result.toString()).toEqual(ADDRESS_BECH32);
});

it('should handle an address instance', () => {
const address = Address.fromB256(ADDRESS_B256);
const result = Address.fromAddressOrBech32String(address);
expect(result.toString()).toEqual(ADDRESS_BECH32);
});

it('should throw FuelError for a non bech-32 string address', async () => {
const address = ADDRESS_B256;
await expectToThrowFuelError(
() => Address.fromAddressOrBech32String(address),
new FuelError(
FuelError.CODES.INVALID_BECH32_ADDRESS,
`Invalid BECH-32 Address: ${address}.`
)
);
});
});
});
20 changes: 20 additions & 0 deletions packages/address/src/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,26 @@ export default class Address extends AbstractAddress {
return typeof address === 'string' ? this.fromString(address) : address;
}

/**
* Takes an ambiguous BECH-32 string or address and creates an `Address`
*
* @returns a new `Address` instance
*/
static fromAddressOrBech32String(address: string | AbstractAddress): AbstractAddress {
if (typeof address === 'string') {
if (isBech32(address)) {
return new Address(address as Bech32Address);
}

throw new FuelError(
FuelError.CODES.INVALID_BECH32_ADDRESS,
`Invalid BECH-32 Address: ${address}.`
);
} else {
return address;
}
}
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved

/**
* Takes a dynamic string or `AbstractAddress` and creates an `Address`
*
Expand Down
33 changes: 18 additions & 15 deletions packages/providers/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,16 +843,17 @@ export default class Provider {
}

async getResourcesForTransaction(
owner: AbstractAddress,
owner: string | AbstractAddress,
danielbate marked this conversation as resolved.
Show resolved Hide resolved
transactionRequestLike: TransactionRequestLike,
forwardingQuantities: CoinQuantity[] = []
) {
const ownerAddress = Address.fromAddressOrBech32String(owner);
const transactionRequest = transactionRequestify(clone(transactionRequestLike));
const transactionCost = await this.getTransactionCost(transactionRequest, forwardingQuantities);

// Add the required resources to the transaction from the owner
transactionRequest.addResources(
await this.getResourcesToSpend(owner, transactionCost.requiredQuantities)
await this.getResourcesToSpend(ownerAddress, transactionCost.requiredQuantities)
);
// Refetch transaction costs with the new resources
// TODO: we could find a way to avoid fetch estimatePredicates again, by returning the transaction or
Expand All @@ -864,7 +865,7 @@ export default class Provider {
transactionRequest,
forwardingQuantities
);
const resources = await this.getResourcesToSpend(owner, requiredQuantities);
const resources = await this.getResourcesToSpend(ownerAddress, requiredQuantities);

return {
resources,
Expand All @@ -878,16 +879,17 @@ export default class Provider {
*/
async getCoins(
/** The address to get coins for */
owner: AbstractAddress,
owner: string | AbstractAddress,
/** The asset ID of coins to get */
assetId?: BytesLike,
/** Pagination arguments */
paginationArgs?: CursorPaginationArgs
): Promise<Coin[]> {
const ownerAddress = Address.fromAddressOrBech32String(owner);
const result = await this.operations.getCoins({
first: 10,
...paginationArgs,
filter: { owner: owner.toB256(), assetId: assetId && hexlify(assetId) },
filter: { owner: ownerAddress.toB256(), assetId: assetId && hexlify(assetId) },
});

const coins = result.coins.edges.map((edge) => edge.node);
Expand All @@ -913,12 +915,13 @@ export default class Provider {
*/
async getResourcesToSpend(
/** The address to get coins for */
owner: AbstractAddress,
owner: string | AbstractAddress,
/** The quantities to get */
quantities: CoinQuantityLike[],
/** IDs of excluded resources from the selection. */
excludedIds?: ExcludeResourcesOption
): Promise<Resource[]> {
const ownerAddress = Address.fromAddressOrBech32String(owner);
const excludeInput = {
messages: excludedIds?.messages?.map((nonce) => hexlify(nonce)) || [],
utxos: excludedIds?.utxos?.map((id) => hexlify(id)) || [],
Expand All @@ -931,7 +934,7 @@ export default class Provider {
excludeInput.utxos = Array.from(uniqueUtxos);
}
const coinsQuery = {
owner: owner.toB256(),
owner: ownerAddress.toB256(),
queryPerAsset: quantities
.map(coinQuantityfy)
.map(({ assetId, amount, max: maxPerAsset }) => ({
Expand Down Expand Up @@ -1108,12 +1111,12 @@ export default class Provider {
*/
async getContractBalance(
/** The contract ID to get the balance for */
contractId: AbstractAddress,
contractId: string | AbstractAddress,
/** The asset ID of coins to get */
assetId: BytesLike
): Promise<BN> {
const { contractBalance } = await this.operations.getContractBalance({
contract: contractId.toB256(),
contract: Address.fromAddressOrBech32String(contractId).toB256(),
asset: hexlify(assetId),
});
return bn(contractBalance.amount, 10);
Expand All @@ -1128,12 +1131,12 @@ export default class Provider {
*/
async getBalance(
/** The address to get coins for */
owner: AbstractAddress,
owner: string | AbstractAddress,
/** The asset ID of coins to get */
assetId: BytesLike
): Promise<BN> {
const { balance } = await this.operations.getBalance({
owner: owner.toB256(),
owner: Address.fromAddressOrBech32String(owner).toB256(),
assetId: hexlify(assetId),
});
return bn(balance.amount, 10);
Expand All @@ -1148,14 +1151,14 @@ export default class Provider {
*/
async getBalances(
/** The address to get coins for */
owner: AbstractAddress,
owner: string | AbstractAddress,
/** Pagination arguments */
paginationArgs?: CursorPaginationArgs
): Promise<CoinQuantity[]> {
const result = await this.operations.getBalances({
first: 10,
...paginationArgs,
filter: { owner: owner.toB256() },
filter: { owner: Address.fromAddressOrBech32String(owner).toB256() },
});

const balances = result.balances.edges.map((edge) => edge.node);
Expand All @@ -1175,14 +1178,14 @@ export default class Provider {
*/
async getMessages(
/** The address to get message from */
address: AbstractAddress,
address: string | AbstractAddress,
/** Pagination arguments */
paginationArgs?: CursorPaginationArgs
): Promise<Message[]> {
const result = await this.operations.getMessages({
first: 10,
...paginationArgs,
owner: address.toB256(),
owner: Address.fromAddressOrBech32String(address).toB256(),
});

const messages = result.messages.edges.map((edge) => edge.node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,9 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
* @param address - The address to get the coin input witness index for.
* @param signature - The signature to update the witness with.
*/
updateWitnessByOwner(address: AbstractAddress, signature: BytesLike) {
const witnessIndex = this.getCoinInputWitnessIndexByOwner(address);
updateWitnessByOwner(address: string | AbstractAddress, signature: BytesLike) {
const bech32Address = Address.fromAddressOrBech32String(address);
const witnessIndex = this.getCoinInputWitnessIndexByOwner(bech32Address);
if (typeof witnessIndex === 'number') {
this.updateWitness(witnessIndex, signature);
}
Expand Down
28 changes: 28 additions & 0 deletions packages/providers/test/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1096,4 +1096,32 @@ describe('Provider', () => {
expect(usedFee.eq(0)).not.toBeTruthy();
expect(minFee.eq(0)).not.toBeTruthy();
});

it('should only accept BECH-32 string addresses in methods that require an address', async () => {
const provider = await Provider.create(FUEL_NETWORK_URL);

const b256Str = Address.fromRandom().toB256();

const methodCalls = [
provider.getBalance(b256Str, BaseAssetId),
provider.getCoins(b256Str),
provider.getResourcesForTransaction(b256Str, new ScriptTransactionRequest()),
provider.getResourcesToSpend(b256Str, []),
provider.getContractBalance(b256Str, BaseAssetId),
provider.getBalances(b256Str),
provider.getMessages(b256Str),
];

const promises = methodCalls.map(async (call) => {
await expectToThrowFuelError(
() => call,
new FuelError(
FuelError.CODES.INVALID_BECH32_ADDRESS,
`Invalid BECH-32 Address: ${b256Str}.`
)
);
});

await Promise.all(promises);
});
});
9 changes: 5 additions & 4 deletions packages/wallet-manager/src/vaults/mnemonic-vault.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Address } from '@fuel-ts/address';
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import type { AbstractAddress } from '@fuel-ts/interfaces';
import { Mnemonic } from '@fuel-ts/mnemonic';
Expand Down Expand Up @@ -82,17 +83,17 @@ export class MnemonicVault implements Vault<MnemonicVaultOptions> {
};
}

exportAccount(address: AbstractAddress): string {
exportAccount(address: string | AbstractAddress): string {
let numberOfAccounts = 0;

const bech32Address = Address.fromAddressOrBech32String(address);
// Look for the account that has the same address
do {
const wallet = Wallet.fromMnemonic(
this.#secret,
this.provider,
this.getDerivePath(numberOfAccounts)
);
if (wallet.address.equals(address)) {
if (wallet.address.equals(bech32Address)) {
return wallet.privateKey;
}
numberOfAccounts += 1;
Expand All @@ -104,7 +105,7 @@ export class MnemonicVault implements Vault<MnemonicVaultOptions> {
);
}

getWallet(address: AbstractAddress): WalletUnlocked {
getWallet(address: string | AbstractAddress): WalletUnlocked {
const privateKey = this.exportAccount(address);
return Wallet.fromPrivateKey(privateKey, this.provider);
}
Expand Down
8 changes: 5 additions & 3 deletions packages/wallet-manager/src/vaults/privatekey-vault.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Address } from '@fuel-ts/address';
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import type { AbstractAddress } from '@fuel-ts/interfaces';
import type { Provider } from '@fuel-ts/providers';
Expand Down Expand Up @@ -64,9 +65,10 @@ export class PrivateKeyVault implements Vault<PkVaultOptions> {
return this.getPublicAccount(wallet.privateKey);
}

exportAccount(address: AbstractAddress): string {
exportAccount(address: string | AbstractAddress): string {
const bech32Address = Address.fromAddressOrBech32String(address);
const privateKey = this.#privateKeys.find((pk) =>
Wallet.fromPrivateKey(pk, this.provider).address.equals(address)
Wallet.fromPrivateKey(pk, this.provider).address.equals(bech32Address)
);

if (!privateKey) {
Expand All @@ -79,7 +81,7 @@ export class PrivateKeyVault implements Vault<PkVaultOptions> {
return privateKey;
}

getWallet(address: AbstractAddress): WalletUnlocked {
getWallet(address: string | AbstractAddress): WalletUnlocked {
const privateKey = this.exportAccount(address);
return Wallet.fromPrivateKey(privateKey, this.provider);
}
Expand Down
15 changes: 9 additions & 6 deletions packages/wallet-manager/src/wallet-manager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Address } from '@fuel-ts/address';
import type { Keystore } from '@fuel-ts/crypto';
import { encrypt, decrypt } from '@fuel-ts/crypto';
import { ErrorCode, FuelError } from '@fuel-ts/errors';
Expand Down Expand Up @@ -110,26 +111,28 @@ export class WalletManager extends EventEmitter {
/**
* Create a Wallet instance for the specific account
*/
getWallet(address: AbstractAddress): WalletUnlocked {
getWallet(address: string | AbstractAddress): WalletUnlocked {
const bech32Address = Address.fromAddressOrBech32String(address);
const vaultState = this.#vaults.find((vs) =>
vs.vault.getAccounts().find((a) => a.address.equals(address))
vs.vault.getAccounts().find((a) => a.address.equals(bech32Address))
);
assert(vaultState, ERROR_MESSAGES.address_not_found);

return vaultState.vault.getWallet(address);
return vaultState.vault.getWallet(bech32Address);
}

/**
* Export specific account privateKey
*/
exportPrivateKey(address: AbstractAddress) {
exportPrivateKey(address: string | AbstractAddress) {
const bech32Address = Address.fromAddressOrBech32String(address);
assert(!this.#isLocked, ERROR_MESSAGES.wallet_not_unlocked);
const vaultState = this.#vaults.find((vs) =>
vs.vault.getAccounts().find((a) => a.address.equals(address))
vs.vault.getAccounts().find((a) => a.address.equals(bech32Address))
);
assert(vaultState, ERROR_MESSAGES.address_not_found);

return vaultState.vault.exportAccount(address);
return vaultState.vault.exportAccount(bech32Address);
}

/**
Expand Down
31 changes: 31 additions & 0 deletions packages/wallet/src/account.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Address } from '@fuel-ts/address';
import { BaseAssetId } from '@fuel-ts/address/configs';
import { FuelError } from '@fuel-ts/errors';
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
import { bn } from '@fuel-ts/math';
import type {
CallResult,
Expand Down Expand Up @@ -467,4 +469,33 @@ describe('Account', () => {
expect(simulate.mock.calls.length).toBe(1);
expect(simulate.mock.calls[0][0]).toEqual(transactionRequest);
});

it('should only accept BECH-32 string addresses in methods that require an address', async () => {
const account = new Account(
'0x09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db',
provider
);

const b256Str = Address.fromRandom().toB256();
const amount = 100;

const methodCalls = [
account.createTransfer(b256Str, amount),
account.transfer(b256Str, amount),
account.transferToContract(b256Str, amount),
account.withdrawToBaseLayer(b256Str, amount),
];

const promises = methodCalls.map(async (call) => {
await expectToThrowFuelError(
() => call,
new FuelError(
FuelError.CODES.INVALID_BECH32_ADDRESS,
`Invalid BECH-32 Address: ${b256Str}.`
)
);
});

await Promise.all(promises);
});
});
Loading
Loading