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: use submitAndAwaitStatus to submit transactions #3101

Merged
merged 16 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-pans-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": patch
---

feat: use `submitAndAwaitStatus` to submit transactions
17 changes: 12 additions & 5 deletions packages/account/src/providers/fuel-graphql-subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,16 @@ export class FuelGraphqlSubscriber implements AsyncIterator<unknown> {
});

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return new FuelGraphqlSubscriber(response.body!.getReader());
const [errorReader, resultReader] = response.body!.tee().map((stream) => stream.getReader());

/**
* If the node threw an error, read it and throw it to the user
* Else just discard the response and return the subscriber below,
* which will have that same response via `resultReader`
*/
await new FuelGraphqlSubscriber(errorReader).next();

return new FuelGraphqlSubscriber(resultReader);
}

private events: Array<{ data: unknown; errors?: { message: string }[] }> = [];
Expand Down Expand Up @@ -94,10 +103,8 @@ export class FuelGraphqlSubscriber implements AsyncIterator<unknown> {
/**
* Gets called when `break` is called in a `for-await-of` loop.
*/
async return(): Promise<IteratorResult<unknown, undefined>> {
await this.stream.cancel();
this.stream.releaseLock();
return { done: true, value: undefined };
return(): Promise<IteratorResult<unknown, undefined>> {
return Promise.resolve({ done: true, value: undefined });
}

[Symbol.asyncIterator](): AsyncIterator<unknown, unknown, undefined> {
Expand Down
6 changes: 6 additions & 0 deletions packages/account/src/providers/operations.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,12 @@ subscription submitAndAwait($encodedTransaction: HexString!) {
}
}

subscription submitAndAwaitStatus($encodedTransaction: HexString!) {
submitAndAwaitStatus(tx: $encodedTransaction) {
...transactionStatusSubscriptionFragment
}
}

subscription statusChange($transactionId: TransactionId!) {
statusChange(id: $transactionId) {
...transactionStatusSubscriptionFragment
Expand Down
26 changes: 20 additions & 6 deletions packages/account/src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,26 @@ type NodeInfoCache = Record<string, NodeInfo>;

type Operations = ReturnType<typeof getOperationsSdk>;

type SdkOperations = Omit<Operations, 'submitAndAwait' | 'statusChange'> & {
type SdkOperations = Omit<
Operations,
'submitAndAwait' | 'statusChange' | 'submitAndAwaitStatus'
> & {
/**
* This method is DEPRECATED and will be REMOVED in v1.
*
* This method will hang until the transaction is fully processed, as described in https://github.com/FuelLabs/fuel-core/issues/2108.
*
* Please use the `submitAndAwaitStatus` method instead.
*/
submitAndAwait: (
...args: Parameters<Operations['submitAndAwait']>
) => Promise<ReturnType<Operations['submitAndAwait']>>;
statusChange: (
...args: Parameters<Operations['statusChange']>
) => Promise<ReturnType<Operations['statusChange']>>;
submitAndAwaitStatus: (
...args: Parameters<Operations['submitAndAwaitStatus']>
) => Promise<ReturnType<Operations['submitAndAwaitStatus']>>;
getBlobs: (variables: { blobIds: string[] }) => Promise<{ blob: { id: string } | null }[]>;
};

Expand Down Expand Up @@ -825,13 +838,14 @@ Supported fuel-core version: ${supportedVersion}.`
if (isTransactionTypeScript(transactionRequest)) {
abis = transactionRequest.abis;
}
const subscription = await this.operations.submitAndAwaitStatus({ encodedTransaction });

const {
submit: { id: transactionId },
} = await this.operations.submit({ encodedTransaction });
this.#cacheInputs(transactionRequest.inputs, transactionId);
this.#cacheInputs(
transactionRequest.inputs,
transactionRequest.getTransactionId(this.getChainId())
);

return new TransactionResponse(transactionRequest, this, abis);
return new TransactionResponse(transactionRequest, this, abis, subscription);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { arrayify, assertUnreachable } from '@fuel-ts/utils';
import type {
GqlMalleableTransactionFieldsFragment,
GqlStatusChangeSubscription,
GqlSubmitAndAwaitStatusSubscription,
} from '../__generated__/operations';
import type Provider from '../provider';
import type { JsonAbisFromAllCalls, TransactionRequest } from '../transaction-request';
Expand Down Expand Up @@ -141,7 +142,12 @@ export class TransactionResponse {
* @param tx - The transaction ID or TransactionRequest.
* @param provider - The provider.
*/
constructor(tx: string | TransactionRequest, provider: Provider, abis?: JsonAbisFromAllCalls) {
constructor(
tx: string | TransactionRequest,
provider: Provider,
abis?: JsonAbisFromAllCalls,
private submitTxSubscription?: AsyncIterable<GqlSubmitAndAwaitStatusSubscription>
) {
this.id = typeof tx === 'string' ? tx : tx.getTransactionId(provider.getChainId());

this.provider = provider;
Expand Down Expand Up @@ -319,11 +325,14 @@ export class TransactionResponse {
return;
}

const subscription = await this.provider.operations.statusChange({
transactionId: this.id,
});
const subscription =
this.submitTxSubscription ??
(await this.provider.operations.statusChange({
transactionId: this.id,
}));

for await (const { statusChange } of subscription) {
for await (const sub of subscription) {
const statusChange = 'statusChange' in sub ? sub.statusChange : sub.submitAndAwaitStatus;
this.status = statusChange;
if (statusChange.type === 'SqueezedOutStatus') {
this.unsetResourceCache();
Expand Down
28 changes: 26 additions & 2 deletions packages/fuel-gauge/src/edge-cases.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TransactionResponse, Wallet } from 'fuels';
import { launchTestNode } from 'fuels/test-utils';
import { ErrorCode, FuelError, hexlify, TransactionResponse, Wallet, WalletUnlocked } from 'fuels';
import { expectToThrowFuelError, launchTestNode } from 'fuels/test-utils';

import { PredicateFalse } from '../test/typegen';
import { CollisionInFnNamesFactory } from '../test/typegen/contracts';

import { launchTestContract } from './utils';
Expand Down Expand Up @@ -53,4 +54,27 @@ describe('Edge Cases', () => {
// we leave this intentionally empty so that we test that the subscription will end the loop when the connection is closed
}
});

test('Submitting a failing transaction via submitAndAwaitStatus subscription throws immediately', async () => {
using launched = await launchTestNode();
const {
wallets: [funder],
} = launched;

const predicate = new PredicateFalse({ provider: launched.provider });
await funder.transfer(predicate.address, 1_000_000);

const transferTx = await predicate.createTransfer(WalletUnlocked.generate().address, 1);

await expectToThrowFuelError(
() =>
launched.provider.operations.submitAndAwaitStatus({
encodedTransaction: hexlify(transferTx.toTransactionBytes()),
}),
new FuelError(
ErrorCode.INVALID_REQUEST,
'Invalid transaction data: PredicateVerificationFailed(Panic(PredicateReturnedNonOne))'
)
);
});
});