Skip to content

Commit

Permalink
feat: inverted submission flow (#3689)
Browse files Browse the repository at this point in the history
* feat: interface change for prepareForSend

* chore: pr release

* feat: further support for the prepareForSend flow

* feat: disable pr release

* chore: remove redundant file

* test: connector account tests

* chore: docs

* chore: changeset

* chore: changeset

* feat: pass connector params to prepareForSend

---------

Co-authored-by: Peter Smith <[email protected]>
  • Loading branch information
danielbate and petertonysmith94 authored Feb 18, 2025
1 parent 5c6c659 commit dcec508
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-apes-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": patch
---

feat: inverted submission flow
21 changes: 21 additions & 0 deletions apps/docs/src/guide/wallets/connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,27 @@ It will return the transaction signature (as a `string`) if it is successfully s

<<< @/../../../packages/account/src/connectors/fuel-connector.ts#fuel-connector-method-sendTransaction{ts:line-numbers}

#### `prepareForSend`

The `prepareForSend` method prepares a transaction for sending. Here the connector should perform all required actions to prepare the transaction i.e additional funding and signing.

The function itself requires two arguments:

- `address` (`string`)
- `transaction` ([`TransactionRequestLike`](DOCS_API_URL/types/_fuel_ts_account.TransactionRequestLike.html))

It will return the prepared transaction (as a [`TransactionRequestLike`](DOCS_API_URL/types/_fuel_ts_account.TransactionRequestLike.html)).

<<< @/../../../packages/account/src/connectors/fuel-connector.ts#fuel-connector-method-prepareForSend{ts:line-numbers}

It can be used in tandem with `Account.sendTransaction` to prepare and send a transaction in one go.

This is enabled by setting the `usePrepareForSend` property to `true` on the connector.

<<< @./snippets/connectors.ts#fuel-connector-method-usePrepareForSend{ts:line-numbers}

This can be beneficial for performance and user experience, as it reduces the number of round trips between the dApp and the network.

#### `assets`

The `assets` method returns a list of all the assets available for the current connection.
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/src/guide/wallets/snippets/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class WalletConnector extends FuelConnector {
};
// #endregion fuel-connector-metadata

// #region fuel-connector-method-usePrepareForSend
public override usePrepareForSend = true;
// #endregion fuel-connector-method-usePrepareForSend

private eventsConnectorEvents(): void {
// #region fuel-connector-events-accounts
const accounts: Array<string> = ['0x1234567890abcdef'];
Expand Down
35 changes: 32 additions & 3 deletions packages/account/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,18 +654,47 @@ export class Account extends AbstractAccount implements WithAddress {
transactionRequestLike: TransactionRequestLike,
{ estimateTxDependencies = true, onBeforeSend, skipCustomFee = false }: AccountSendTxParams = {}
): Promise<TransactionResponse> {
// Check if the account is using a connector, and therefore we do not have direct access to the
// private key.
if (this._connector) {
return this.provider.getTransactionResponse(
await this._connector.sendTransaction(this.address.toString(), transactionRequestLike, {
// If the connector is using prepareForSend, the connector will prepare the transaction for the dapp,
// and submission is owned by the dapp. This reduces network requests to submit and create the
// summary for a tx.
if (this._connector.usePrepareForSend) {
const preparedTransaction = await this._connector.prepareForSend(
this.address.toString(),
transactionRequestLike,
{
onBeforeSend,
skipCustomFee,
}
);
// Submit the prepared transaction using the provider.
return this.provider.sendTransaction(preparedTransaction, {
estimateTxDependencies: false,
});
}

// Otherwise, the connector itself will submit the transaction, and the app will use
// the tx id to create the summary, requiring multiple network requests.
const txId = await this._connector.sendTransaction(
this.address.toString(),
transactionRequestLike,
{
onBeforeSend,
skipCustomFee,
})
}
);
// And return the transaction response for the returned tx id.
return this.provider.getTransactionResponse(txId);
}

const transactionRequest = transactionRequestify(transactionRequestLike);

if (estimateTxDependencies) {
await this.provider.estimateTxDependencies(transactionRequest);
}

return this.provider.sendTransaction(transactionRequest, {
estimateTxDependencies: false,
});
Expand Down
26 changes: 26 additions & 0 deletions packages/account/src/connectors/fuel-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ interface Connector {
params?: FuelConnectorSendTxParams
): Promise<string>;
// #endregion fuel-connector-method-sendTransaction
// #region fuel-connector-method-prepareForSend
prepareForSend(
address: string,
transaction: TransactionRequestLike
): Promise<TransactionRequestLike>;
// #endregion fuel-connector-method-prepareForSend
// #region fuel-connector-method-currentAccount
currentAccount(): Promise<string | null>;
// #endregion fuel-connector-method-currentAccount
Expand Down Expand Up @@ -97,6 +103,7 @@ export abstract class FuelConnector extends EventEmitter implements Connector {
installed: boolean = false;
external: boolean = true;
events = FuelConnectorEventTypes;
usePrepareForSend: boolean = false;

/**
* Should return true if the connector is loaded
Expand Down Expand Up @@ -209,6 +216,25 @@ export abstract class FuelConnector extends EventEmitter implements Connector {
throw new FuelError(FuelError.CODES.NOT_IMPLEMENTED, 'Method not implemented.');
}

/**
* Should perform all necessary operations (i.e estimation,
* funding, signing) to prepare a tx so it can be submitted
* at the app level.
*
* @param address - The address to sign the tx
* @param transaction - The tx to prepare
* @param params - Optional parameters to send the transactions
*
* @returns The prepared tx request
*/
async prepareForSend(
_address: string,
_transaction: TransactionRequestLike,
_params?: FuelConnectorSendTxParams
): Promise<TransactionRequestLike> {
throw new FuelError(FuelError.CODES.NOT_IMPLEMENTED, 'Method not implemented.');
}

/**
* Should return the current account selected inside the connector, if the account
* is authorized for the connection.
Expand Down
1 change: 1 addition & 0 deletions packages/account/src/connectors/fuel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export class Fuel extends FuelConnector implements FuelSdk {
const { installed } = await this.fetchConnectorStatus(connector);
if (installed) {
this._currentConnector = connector;
this.usePrepareForSend = connector.usePrepareForSend;
this.emit(this.events.currentConnector, connector);
this.setupConnectorEvents(Object.values(FuelConnectorEventTypes));
await this._storage?.setItem(Fuel.STORAGE_KEY, connector.name);
Expand Down
1 change: 1 addition & 0 deletions packages/account/src/connectors/types/connector-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum FuelConnectorMethods {
// Signature methods
signMessage = 'signMessage',
sendTransaction = 'sendTransaction',
prepareForSend = 'prepareForSend',
// Assets metadata methods
assets = 'assets',
addAsset = 'addAsset',
Expand Down
23 changes: 23 additions & 0 deletions packages/account/test/fixtures/mocked-prep-connector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { transactionRequestify, type TransactionRequestLike } from '../../src';

import { MockConnector } from './mocked-connector';

export class MockedPrepConnector extends MockConnector {
override usePrepareForSend = true;

override async prepareForSend(
address: string,
transaction: TransactionRequestLike
): Promise<TransactionRequestLike> {
const wallet = this._wallets.find((w) => w.address.toString() === address);
if (!wallet) {
throw new Error('Wallet is not found!');
}

const request = transactionRequestify(transaction);
const signature = await wallet.signTransaction(request);
request.updateWitnessByOwner(wallet.address, signature);

return request;
}
}
157 changes: 156 additions & 1 deletion packages/account/test/fuel-wallet-connector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { bn } from '@fuel-ts/math';
import type { BytesLike } from '@fuel-ts/utils';
import { EventEmitter } from 'events';

import type { AccountSendTxParams, Network, ProviderOptions, SelectNetworkArguments } from '../src';
import {
Account,
type AccountSendTxParams,
type Network,
type ProviderOptions,
type SelectNetworkArguments,
} from '../src';
import { TESTNET_NETWORK_URL } from '../src/configs';
import { Fuel } from '../src/connectors/fuel';
import { FuelConnectorEventType } from '../src/connectors/types';
Expand All @@ -16,6 +22,7 @@ import { setupTestProviderAndWallets, TestMessage } from '../src/test-utils';
import { Wallet } from '../src/wallet';

import { MockConnector } from './fixtures/mocked-connector';
import { MockedPrepConnector } from './fixtures/mocked-prep-connector';
import { promiseCallback } from './fixtures/promise-callback';

/**
Expand Down Expand Up @@ -684,6 +691,7 @@ describe('Fuel Connector', () => {
});

const sendTransactionSpy = vi.spyOn(connectorWallet, 'sendTransaction');
const prepareForSendSpy = vi.spyOn(connector, 'prepareForSend');

const request = new ScriptTransactionRequest();
const resources = await connectorWallet.getResourcesToSpend([
Expand All @@ -702,6 +710,153 @@ describe('Fuel Connector', () => {
params
);
expect(response).toBeDefined();
// transaction prepared and sent via connector
expect(sendTransactionSpy).toHaveBeenCalledWith(request, params);
expect(prepareForSendSpy).not.toHaveBeenCalled();
});

it('should ensure prepareForSend works just fine', async () => {
using launched = await setupTestProviderAndWallets();
const {
provider,
wallets: [connectorWallet],
} = launched;
const connector = new MockedPrepConnector({
wallets: [connectorWallet],
});
const fuel = await new Fuel({
connectors: [connector],
}).init();

const connectorPrepareForSendSpy = vi.spyOn(connector, 'prepareForSend');

const request = new ScriptTransactionRequest();
const resources = await connectorWallet.getResourcesToSpend([
{ assetId: await provider.getBaseAssetId(), amount: 1000 },
]);
request.addResources(resources);
await request.estimateAndFund(connectorWallet);

const address = connectorWallet.address.toString();

const params: AccountSendTxParams = {
onBeforeSend: vi.fn(),
skipCustomFee: true,
};

const tx = await fuel.prepareForSend(address, request, params);

expect(tx).toBeDefined();
expect(connectorPrepareForSendSpy).toHaveBeenCalledWith(address, request, params);
});

it('should ensure account send transaction with connector works just fine', async () => {
using launched = await setupTestProviderAndWallets();
const {
provider,
wallets: [connectorWallet],
} = launched;
const connector = new MockedPrepConnector({
wallets: [connectorWallet],
});
const fuel = await new Fuel({
connectors: [connector],
}).init();

const account = new Account(connectorWallet.address.toString(), provider, fuel);

const providerSendTransactionSpy = vi.spyOn(provider, 'sendTransaction');
const connectorSendTransactionSpy = vi.spyOn(connectorWallet, 'sendTransaction');
const connectorPrepareForSendSpy = vi.spyOn(connector, 'prepareForSend');

const request = new ScriptTransactionRequest();
const resources = await connectorWallet.getResourcesToSpend([
{ assetId: await provider.getBaseAssetId(), amount: 1000 },
]);
request.addResources(resources);
await request.estimateAndFund(connectorWallet);

const address = connectorWallet.address.toString();
const params: AccountSendTxParams = {
onBeforeSend: vi.fn(),
skipCustomFee: true,
};

const tx = await account.sendTransaction(request, params);
expect(tx).toBeDefined();

// transaction prepared via connector and sent via provider
expect(connectorPrepareForSendSpy).toHaveBeenCalledWith(address, request, params);
expect(providerSendTransactionSpy).toHaveBeenCalled();
// not sent via connector
expect(connectorSendTransactionSpy).not.toHaveBeenCalled();
});

it('assembles tx result from connector account [w/o prepareForSend]', async () => {
using launched = await setupTestProviderAndWallets();
const {
provider,
wallets: [connectorWallet, recipientWallet],
} = launched;
const connector = new MockConnector({
wallets: [connectorWallet],
});
const fuel = await new Fuel({
connectors: [connector],
}).init();

const connectorAccount = new Account(connectorWallet.address.toString(), provider, fuel);
const recipientAddress = recipientWallet.address.toString();

const submitAndAwaitStatusSpy = vi.spyOn(provider.operations, 'submitAndAwaitStatus');
const statusChangeSpy = vi.spyOn(provider.operations, 'statusChange');
const getTransactionWithReceiptsSpy = vi.spyOn(
provider.operations,
'getTransactionWithReceipts'
);

const tx = await connectorAccount.transfer(recipientAddress, 1000);
const result = await tx.waitForResult();

expect(result.isStatusSuccess).toBe(true);
expect(result.receipts.length).toBe(2);
expect(result.transaction.inputs).toBeDefined();
expect(submitAndAwaitStatusSpy).toHaveBeenCalled();
expect(statusChangeSpy).toHaveBeenCalled();
expect(getTransactionWithReceiptsSpy).toHaveBeenCalled();
});

it('assembles tx result from connector account [w/ prepareForSend]', async () => {
using launched = await setupTestProviderAndWallets();
const {
provider,
wallets: [connectorWallet, recipientWallet],
} = launched;
const connector = new MockedPrepConnector({
wallets: [connectorWallet],
});
const fuel = await new Fuel({
connectors: [connector],
}).init();

const connectorAccount = new Account(connectorWallet.address.toString(), provider, fuel);
const recipientAddress = recipientWallet.address.toString();

const submitAndAwaitStatusSpy = vi.spyOn(provider.operations, 'submitAndAwaitStatus');
const statusChangeSpy = vi.spyOn(provider.operations, 'statusChange');
const getTransactionWithReceiptsSpy = vi.spyOn(
provider.operations,
'getTransactionWithReceipts'
);

const tx = await connectorAccount.transfer(recipientAddress, 1000);
const result = await tx.waitForResult();

expect(result.isStatusSuccess).toBe(true);
expect(result.receipts.length).toBe(2);
expect(result.transaction.inputs).toBeDefined();
expect(submitAndAwaitStatusSpy).toHaveBeenCalled();
expect(statusChangeSpy).not.toHaveBeenCalled();
expect(getTransactionWithReceiptsSpy).not.toHaveBeenCalled();
});
});

0 comments on commit dcec508

Please sign in to comment.