From d44de76bdde4d566e0bac6e872adc6e6f29f0bee Mon Sep 17 00:00:00 2001 From: Cameron Manavian Date: Wed, 9 Nov 2022 17:11:18 -0800 Subject: [PATCH] feat!: update fund to use total resources (#589) * break things * add cs * remove coins to spend * fix property checl --- .changeset/quick-mugs-clap.md | 6 ++ .../functions/base-invocation-scope.ts | 4 +- packages/providers/src/coin.ts | 3 +- packages/providers/src/provider.ts | 68 +++++++++--------- packages/providers/src/resource.ts | 13 +++- .../transaction-request.ts | 70 +++++++++++++++++-- packages/script/src/script.test.ts | 4 +- packages/wallet/src/base-locked-wallet.ts | 33 ++++----- packages/wallet/src/test-utils.ts | 4 +- packages/wallet/src/transfer.test.ts | 12 ++-- packages/wallet/src/wallet-locked.test.ts | 6 +- 11 files changed, 148 insertions(+), 75 deletions(-) create mode 100644 .changeset/quick-mugs-clap.md diff --git a/.changeset/quick-mugs-clap.md b/.changeset/quick-mugs-clap.md new file mode 100644 index 00000000000..913b6411612 --- /dev/null +++ b/.changeset/quick-mugs-clap.md @@ -0,0 +1,6 @@ +--- +"@fuel-ts/providers": patch +"@fuel-ts/wallet": patch +--- + +Use resources for fund diff --git a/packages/contract/src/contracts/functions/base-invocation-scope.ts b/packages/contract/src/contracts/functions/base-invocation-scope.ts index 9fd996bd56c..88144ed6a9f 100644 --- a/packages/contract/src/contracts/functions/base-invocation-scope.ts +++ b/packages/contract/src/contracts/functions/base-invocation-scope.ts @@ -163,8 +163,8 @@ export class BaseInvocationScope { this.transactionRequest.inputs = this.transactionRequest.inputs.filter( (i) => i.type !== InputType.Coin ); - const coins = await this.contract.wallet?.getCoinsToSpend(this.requiredCoins); - this.transactionRequest.addCoins(coins || []); + const resources = await this.contract.wallet?.getResourcesToSpend(this.requiredCoins); + this.transactionRequest.addResources(resources || []); return this; } diff --git a/packages/providers/src/coin.ts b/packages/providers/src/coin.ts index 6961bcb7233..dd2b089ee84 100644 --- a/packages/providers/src/coin.ts +++ b/packages/providers/src/coin.ts @@ -1,3 +1,4 @@ +import type { AbstractAddress } from '@fuel-ts/interfaces'; import type { BN } from '@fuel-ts/math'; import { GqlCoinStatus as CoinStatus } from './__generated__/operations'; @@ -9,7 +10,7 @@ export type Coin = { id: string; assetId: string; amount: BN; - owner: string; + owner: AbstractAddress; status: CoinStatus; maturity: number; blockCreated: BN; diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 9a191580c91..4452e4cae71 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -32,8 +32,8 @@ import type { Coin } from './coin'; import type { CoinQuantity, CoinQuantityLike } from './coin-quantity'; import { coinQuantityfy } from './coin-quantity'; import type { Message, MessageProof } from './message'; -import type { ExcludeResourcesOption, RawCoin, Resources } from './resource'; -import { isCoin } from './resource'; +import type { ExcludeResourcesOption, Resource } from './resource'; +import { isRawCoin } from './resource'; import { ScriptTransactionRequest, transactionRequestify } from './transaction-request'; import type { TransactionRequestLike, TransactionRequest } from './transaction-request'; import type { @@ -402,7 +402,7 @@ export default class Provider { id: coin.utxoId, assetId: coin.assetId, amount: bn(coin.amount), - owner: coin.owner, + owner: Address.fromAddressOrString(coin.owner), status: coin.status, maturity: bn(coin.maturity).toNumber(), blockCreated: bn(coin.blockCreated), @@ -419,7 +419,7 @@ export default class Provider { quantities: CoinQuantityLike[], /** IDs of excluded resources from the selection. */ excludedIds?: ExcludeResourcesOption - ): Promise { + ): Promise { const excludeInput = { messages: excludedIds?.messages?.map((id) => hexlify(id)) || [], utxos: excludedIds?.utxos?.map((id) => hexlify(id)) || [], @@ -436,33 +436,29 @@ export default class Provider { excludedIds: excludeInput, }); - return result.resourcesToSpend; - } - - /** - * Returns coins for the given owner satisfying the spend query - */ - async getCoinsToSpend( - /** The address to get coins for */ - owner: AbstractAddress, - /** The quantities to get */ - quantities: CoinQuantityLike[], - /** IDs of coins to exclude */ - excludedIds?: BytesLike[] - ): Promise { - const resources = await this.getResourcesToSpend(owner, quantities, { utxos: excludedIds }); - - const coins = resources.flat().filter(isCoin) as RawCoin[]; + return result.resourcesToSpend.flat().map((resource) => { + if (isRawCoin(resource)) { + return { + id: resource.utxoId, + amount: bn(resource.amount), + status: resource.status, + assetId: resource.assetId, + owner: Address.fromAddressOrString(resource.owner), + maturity: bn(resource.maturity).toNumber(), + blockCreated: bn(resource.blockCreated), + }; + } - return coins.map((coin) => ({ - id: coin.utxoId, - status: coin.status, - assetId: coin.assetId, - amount: bn(coin.amount), - owner: coin.owner, - maturity: bn(coin.maturity).toNumber(), - blockCreated: bn(coin.blockCreated), - })); + return { + sender: Address.fromAddressOrString(resource.sender), + recipient: Address.fromAddressOrString(resource.recipient), + nonce: bn(resource.nonce), + amount: bn(resource.amount), + data: InputMessageCoder.decodeData(resource.data), + daHeight: bn(resource.daHeight), + fuelBlockSpend: bn(resource.fuelBlockSpend), + }; + }); } /** @@ -670,7 +666,7 @@ export default class Provider { predicateOptions?: BuildPredicateOptions, walletAddress?: AbstractAddress ): Promise { - const predicateCoins: Coin[] = await this.getCoinsToSpend(predicate.address, [ + const predicateResources: Resource[] = await this.getResourcesToSpend(predicate.address, [ [amountToSpend, assetId], ]); const options = { @@ -688,12 +684,12 @@ export default class Provider { encoded = abiCoder.encode(predicate.types, predicateData); } - const totalInPredicate: BN = predicateCoins.reduce((prev: BN, coin: Coin) => { - request.addCoin({ + const totalInPredicate: BN = predicateResources.reduce((prev: BN, coin: Resource) => { + request.addResource({ ...coin, predicate: predicate.bytes, predicateData: encoded, - } as Coin); + } as unknown as Resource); request.outputs = []; return prev.add(coin.amount); @@ -708,8 +704,8 @@ export default class Provider { } if (requiredCoinQuantities.length && walletAddress) { - const coins = await this.getCoinsToSpend(walletAddress, requiredCoinQuantities); - request.addCoins(coins); + const resources = await this.getResourcesToSpend(walletAddress, requiredCoinQuantities); + request.addResources(resources); } return request; diff --git a/packages/providers/src/resource.ts b/packages/providers/src/resource.ts index da282792720..289b99d3d7f 100644 --- a/packages/providers/src/resource.ts +++ b/packages/providers/src/resource.ts @@ -1,6 +1,8 @@ import type { BytesLike } from '@ethersproject/bytes'; import type { GqlGetResourcesToSpendQuery, GqlCoinStatus } from './__generated__/operations'; +import type { Coin } from './coin'; +import type { Message } from './message'; export type RawCoin = { utxoId: string; @@ -21,7 +23,8 @@ export type RawMessage = { daHeight: string; }; -type Resource = RawCoin | RawMessage; +export type RawResource = RawCoin | RawMessage; +export type Resource = Coin | Message; export type Resources = GqlGetResourcesToSpendQuery['resourcesToSpend']; @@ -30,5 +33,9 @@ export type ExcludeResourcesOption = { messages?: BytesLike[]; }; -export const isCoin = (resource: Resource) => 'utxoId' in resource; -export const isMessage = (resource: Resource) => 'recipient' in resource; +export const isRawCoin = (resource: RawResource): resource is RawCoin => 'utxoId' in resource; +export const isRawMessage = (resource: RawResource): resource is RawMessage => + 'recipient' in resource; + +export const isCoin = (resource: Resource): resource is Coin => 'id' in resource; +export const isMessage = (resource: Resource): resource is Message => 'recipient' in resource; diff --git a/packages/providers/src/transaction-request/transaction-request.ts b/packages/providers/src/transaction-request/transaction-request.ts index 1658ec10364..829f31b0a02 100644 --- a/packages/providers/src/transaction-request/transaction-request.ts +++ b/packages/providers/src/transaction-request/transaction-request.ts @@ -1,7 +1,7 @@ /* eslint-disable max-classes-per-file */ import type { BytesLike } from '@ethersproject/bytes'; import { arrayify, hexlify } from '@ethersproject/bytes'; -import { addressify, Address } from '@fuel-ts/address'; +import { addressify } from '@fuel-ts/address'; import { NativeAssetId, ZeroBytes32 } from '@fuel-ts/constants'; import type { AddressLike, @@ -24,6 +24,8 @@ import type { Coin } from '../coin'; import type { CoinQuantity, CoinQuantityLike } from '../coin-quantity'; import { coinQuantityfy } from '../coin-quantity'; import type { Message } from '../message'; +import type { Resource } from '../resource'; +import { isCoin } from '../resource'; import { arraifyFromUint8Array, calculatePriceWithFactor } from '../util'; import type { @@ -36,6 +38,7 @@ import type { TransactionRequestInput, CoinTransactionRequestInput, ContractTransactionRequestInput, + MessageTransactionRequestInput, } from './input'; import { inputify } from './input'; import type { TransactionRequestOutput, ChangeTransactionRequestOutput } from './output'; @@ -254,11 +257,69 @@ abstract class BaseTransactionRequest implements BaseTransactionRequestLike { this.updateWitness(witnessIndex, witness); } + /** + * Converts the given Resource to a ResourceInput with the appropriate witnessIndex and pushes it + */ + addResource(resource: Resource) { + const ownerAddress = isCoin(resource) ? resource.owner : resource.recipient; + const assetId = isCoin(resource) ? resource.assetId : NativeAssetId; + const type = isCoin(resource) ? InputType.Coin : InputType.Message; + let witnessIndex = this.getCoinInputWitnessIndexByOwner(ownerAddress); + + // Insert a dummy witness if no witness exists + if (typeof witnessIndex !== 'number') { + witnessIndex = this.createWitness(); + } + + // Insert the Input + this.pushInput( + isCoin(resource) + ? ({ + type, + ...resource, + owner: resource.owner.toB256(), + witnessIndex, + txPointer: '0x00000000000000000000000000000000', + } as CoinTransactionRequestInput) + : ({ + type, + ...resource, + sender: resource.sender.toB256(), + recipient: resource.recipient.toB256(), + witnessIndex, + txPointer: '0x00000000000000000000000000000000', + } as MessageTransactionRequestInput) + ); + + // Find the ChangeOutput for the AssetId of the Resource + const changeOutput = this.getChangeOutputs().find( + (output) => hexlify(output.assetId) === assetId + ); + + // Throw if the existing ChangeOutput is not for the same owner + if (changeOutput && hexlify(changeOutput.to) !== ownerAddress.toB256()) { + throw new ChangeOutputCollisionError(); + } + + // Insert a ChangeOutput if it does not exist + if (!changeOutput) { + this.pushOutput({ + type: OutputType.Change, + to: ownerAddress.toB256(), + assetId, + }); + } + } + + addResources(resources: ReadonlyArray) { + resources.forEach((resource) => this.addResource(resource)); + } + /** * Converts the given Coin to a CoinInput with the appropriate witnessIndex and pushes it */ addCoin(coin: Coin) { - let witnessIndex = this.getCoinInputWitnessIndexByOwner(Address.fromB256(coin.owner)); + let witnessIndex = this.getCoinInputWitnessIndexByOwner(coin.owner); // Insert a dummy witness if no witness exists if (typeof witnessIndex !== 'number') { @@ -269,6 +330,7 @@ abstract class BaseTransactionRequest implements BaseTransactionRequestLike { this.pushInput({ type: InputType.Coin, ...coin, + owner: coin.owner.toB256(), witnessIndex, txPointer: '0x00000000000000000000000000000000', }); @@ -279,7 +341,7 @@ abstract class BaseTransactionRequest implements BaseTransactionRequestLike { ); // Throw if the existing ChangeOutput is not for the same owner - if (changeOutput && hexlify(changeOutput.to) !== coin.owner) { + if (changeOutput && hexlify(changeOutput.to) !== coin.owner.toB256()) { throw new ChangeOutputCollisionError(); } @@ -287,7 +349,7 @@ abstract class BaseTransactionRequest implements BaseTransactionRequestLike { if (!changeOutput) { this.pushOutput({ type: OutputType.Change, - to: coin.owner, + to: coin.owner.toB256(), assetId: coin.assetId, }); } diff --git a/packages/script/src/script.test.ts b/packages/script/src/script.test.ts index 641cb08824f..d6efda364b0 100644 --- a/packages/script/src/script.test.ts +++ b/packages/script/src/script.test.ts @@ -44,8 +44,8 @@ const callScript = async ( // Get and add required coins to the transaction if (requiredCoinQuantities.length) { - const coins = await wallet.getCoinsToSpend(requiredCoinQuantities); - request.addCoins(coins); + const resources = await wallet.getResourcesToSpend(requiredCoinQuantities); + request.addResources(resources); } const response = await wallet.sendTransaction(request); diff --git a/packages/wallet/src/base-locked-wallet.ts b/packages/wallet/src/base-locked-wallet.ts index 5f5a2e88317..1add75d341e 100644 --- a/packages/wallet/src/base-locked-wallet.ts +++ b/packages/wallet/src/base-locked-wallet.ts @@ -18,6 +18,8 @@ import type { BuildPredicateOptions, TransactionResult, Message, + Resource, + ExcludeResourcesOption, } from '@fuel-ts/providers'; import { withdrawScript, @@ -66,14 +68,13 @@ export class BaseWalletLocked extends AbstractWallet { } /** - * Returns coins satisfying the spend query. + * Returns resources satisfying the spend query. */ - async getCoinsToSpend( - quantities: CoinQuantityLike[], - /** IDs of coins to exclude */ - excludedIds?: BytesLike[] - ): Promise { - return this.provider.getCoinsToSpend(this.address, quantities, excludedIds); + async getResourcesToSpend( + quantities: CoinQuantityLike[] /** IDs of coins to exclude */, + excludedIds?: ExcludeResourcesOption + ): Promise { + return this.provider.getResourcesToSpend(this.address, quantities, excludedIds); } /** @@ -172,13 +173,13 @@ export class BaseWalletLocked extends AbstractWallet { } /** - * Adds coins to the transaction enough to fund it. + * Adds resources to the transaction enough to fund it. */ async fund(request: T): Promise { const fee = request.calculateFee(); - const coins = await this.getCoinsToSpend([fee]); + const resources = await this.getResourcesToSpend([fee]); - request.addCoins(coins); + request.addResources(resources); } /** @@ -205,8 +206,8 @@ export class BaseWalletLocked extends AbstractWallet { } else { quantities = [[amount, assetId], fee]; } - const coins = await this.getCoinsToSpend(quantities); - request.addCoins(coins); + const resources = await this.getResourcesToSpend(quantities); + request.addResources(resources); return this.sendTransaction(request); } @@ -243,8 +244,8 @@ export class BaseWalletLocked extends AbstractWallet { let quantities: CoinQuantityLike[] = []; fee.amount.add(amount); quantities = [fee]; - const coins = await this.getCoinsToSpend(quantities); - request.addCoins(coins); + const resources = await this.getResourcesToSpend(quantities); + request.addResources(resources); return this.sendTransaction(request); } @@ -299,8 +300,8 @@ export class BaseWalletLocked extends AbstractWallet { } if (requiredCoinQuantities.length) { - const coins = await this.getCoinsToSpend(requiredCoinQuantities); - request.addCoins(coins); + const resources = await this.getResourcesToSpend(requiredCoinQuantities); + request.addResources(resources); } return request; diff --git a/packages/wallet/src/test-utils.ts b/packages/wallet/src/test-utils.ts index c0bfd43ccb2..e246f01b547 100644 --- a/packages/wallet/src/test-utils.ts +++ b/packages/wallet/src/test-utils.ts @@ -11,13 +11,13 @@ export const seedWallet = async (wallet: WalletUnlocked, quantities: CoinQuantit wallet.provider ); // Connect to the same Provider as wallet - const coins = await genesisWallet.getCoinsToSpend(quantities); + const resources = await genesisWallet.getResourcesToSpend(quantities); // Create transaction const request = new ScriptTransactionRequest({ gasLimit: 10000, gasPrice: 1, }); - request.addCoins(coins); + request.addResources(resources); quantities .map(coinQuantityfy) .forEach(({ amount, assetId }) => request.addCoinOutput(wallet.address, amount, assetId)); diff --git a/packages/wallet/src/transfer.test.ts b/packages/wallet/src/transfer.test.ts index 673b94b2f4d..a9d1e76d604 100644 --- a/packages/wallet/src/transfer.test.ts +++ b/packages/wallet/src/transfer.test.ts @@ -47,7 +47,7 @@ describe('Wallet', () => { expect(receiverBalances).toEqual([{ assetId: NativeAssetId, amount: bn(1) }]); }); - it('can exclude IDs when getCoinsToSpend is called', async () => { + it('can exclude IDs when getResourcesToSpend is called', async () => { const provider = new Provider('http://127.0.0.1:4000/graphql'); const assetIdA = '0x0101010101010101010101010101010101010101010101010101010101010101'; @@ -62,9 +62,9 @@ describe('Wallet', () => { const coins = await user.getCoins(); // Test excludes the UTXO where the assetIdA gets added to the senders wallet - await expect(user.getCoinsToSpend([[1, assetIdA, 100]], [coins[0].id])).rejects.toThrow( - /not enough resources to fit the target/ - ); + await expect( + user.getResourcesToSpend([[1, assetIdA, 100]], { utxos: [coins[0].id] }) + ).rejects.toThrow(/not enough resources to fit the target/); }); it('can transfer multiple types of coins to multiple destinations', async () => { @@ -83,12 +83,12 @@ describe('Wallet', () => { const receiverA = await generateTestWallet(provider); const receiverB = await generateTestWallet(provider); - const coins = await sender.getCoinsToSpend([ + const resources = await sender.getResourcesToSpend([ [amount * 2, assetIdA], [amount * 2, assetIdB], ]); - request.addCoins(coins); + request.addResources(resources); request.addCoinOutputs(receiverA, [ [amount, assetIdA], [amount, assetIdB], diff --git a/packages/wallet/src/wallet-locked.test.ts b/packages/wallet/src/wallet-locked.test.ts index 719a82ae5ae..6e86626000c 100644 --- a/packages/wallet/src/wallet-locked.test.ts +++ b/packages/wallet/src/wallet-locked.test.ts @@ -34,17 +34,17 @@ describe('WalletLocked', () => { expect(assetC?.amount.gt(1)).toBeTruthy(); }); - it('getCoinsToSpend()', async () => { + it('getResourcesToSpend()', async () => { const walletLocked = Wallet.fromAddress( '0x09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db' ); - const coinToSpend = await walletLocked.getCoinsToSpend([ + const resourcesToSpend = await walletLocked.getResourcesToSpend([ { amount: bn(2), assetId: '0x0101010101010101010101010101010101010101010101010101010101010101', }, ]); - expect(coinToSpend[0].amount.gt(2)).toBeTruthy(); + expect(resourcesToSpend[0].amount.gt(2)).toBeTruthy(); }); it('getMessages()', async () => {