Skip to content

Commit

Permalink
feat!: read malleable fields from transaction status on subscription
Browse files Browse the repository at this point in the history
  • Loading branch information
nedsalk committed Aug 16, 2024
1 parent 5727b17 commit 98aaafd
Show file tree
Hide file tree
Showing 16 changed files with 273 additions and 62 deletions.
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": minor
---

feat!: read malleable fields from transaction status on subscription
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,7 @@ describe('querying the chain', () => {
const { nonce } = result.receipts[0] as TransactionResultMessageOutReceipt;

// Retrieves the message proof for the transaction ID and nonce using the next block Id
const messageProof = await provider.getMessageProof(
result.gqlTransaction.id,
nonce,
latestBlock?.id
);
const messageProof = await provider.getMessageProof(result.id, nonce, latestBlock?.id);
// #endregion Message-getMessageProof-blockId

expect(messageProof?.amount.toNumber()).toEqual(100);
Expand Down Expand Up @@ -283,7 +279,7 @@ describe('querying the chain', () => {

// Retrieves the message proof for the transaction ID and nonce using the block height
const messageProof = await provider.getMessageProof(
result.gqlTransaction.id,
result.id,
nonce,
undefined,
latestBlock?.height
Expand Down
1 change: 0 additions & 1 deletion apps/docs/src/guide/transactions/transaction-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ Once a transaction has been submitted, you may want to extract information regar
- The status (submitted, success, squeezed out, or failure)
- Receipts (return data, logs, mints/burns, transfers and panic/reverts)
- Gas fees and usages
- The raw payload of the transaction including inputs and outputs
- Date and time of the transaction
- The block the transaction was included in

Expand Down
4 changes: 2 additions & 2 deletions packages/account/src/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ describe('Account', () => {

const messageOutReceipt = <providersMod.TransactionResultMessageOutReceipt>result.receipts[0];
const messageProof = await provider.getMessageProof(
result.gqlTransaction.id,
result.id,
messageOutReceipt.nonce,
nextBlock.blockId
);
Expand Down Expand Up @@ -763,7 +763,7 @@ describe('Account', () => {

const messageOutReceipt = <providersMod.TransactionResultMessageOutReceipt>result.receipts[0];
expect(result.isStatusSuccess).toBeTruthy();
expect(result.gqlTransaction.id).toEqual(messageOutReceipt.sender);
expect(result.id).toEqual(messageOutReceipt.sender);
expect(recipient.toHexString()).toEqual(messageOutReceipt.recipient);
expect(amount.toString()).toEqual(messageOutReceipt.amount.toString());
});
Expand Down
58 changes: 56 additions & 2 deletions packages/account/src/providers/operations.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,64 @@ fragment transactionStatusFragment on TransactionStatus {
}
}

fragment malleableFieldsFragment on Transaction {
receiptsRoot
inputs {
type: __typename
... on InputCoin {
txPointer
}
... on InputContract {
txPointer
}
}
outputs {
type: __typename
... on CoinOutput {
to
amount
assetId
}
... on ContractOutput {
inputIndex
balanceRoot
stateRoot
}
... on ChangeOutput {
to
amount
assetId
}
... on VariableOutput {
to
amount
assetId
}
... on ContractCreated {
contract
stateRoot
}
}
}

fragment transactionStatusSubscriptionFragment on TransactionStatus {
type: __typename
... on SubmittedStatus {
...SubmittedStatusFragment
}
... on SuccessStatus {
...SuccessStatusFragment
transaction {
...malleableFieldsFragment
}
}
... on FailureStatus {
...FailureStatusFragment
transaction {
...malleableFieldsFragment
}
}
... on SqueezedOutStatus {
reason
...SqueezedOutStatusFragment
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/account/src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ Supported fuel-core version: ${supportedVersion}.`
} = await this.operations.submit({ encodedTransaction });
this.#cacheInputs(transactionRequest.inputs, transactionId);

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

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,23 @@ import type {
Transaction,
ReceiptMint,
ReceiptBurn,
OutputCoin,
OutputContract,
OutputChange,
OutputVariable,
OutputContractCreated,
Output,
TransactionType,
} from '@fuel-ts/transactions';
import { TransactionCoder } from '@fuel-ts/transactions';
import { arrayify, sleep } from '@fuel-ts/utils';
import { OutputType, TransactionCoder, TxPointerCoder } from '@fuel-ts/transactions';
import { arrayify, assertUnreachable, sleep } from '@fuel-ts/utils';

import type { GqlReceiptFragment } from '../__generated__/operations';
import type {
GqlMalleableFieldsFragment,
GqlStatusChangeSubscription,
} from '../__generated__/operations';
import type Provider from '../provider';
import type { JsonAbisFromAllCalls } from '../transaction-request';
import type { JsonAbisFromAllCalls, TransactionRequest } from '../transaction-request';
import { assembleTransactionSummary } from '../transaction-summary/assemble-transaction-summary';
import { processGqlReceipt } from '../transaction-summary/receipt';
import type {
Expand Down Expand Up @@ -78,10 +88,40 @@ export type TransactionResultReceipt =

/** @hidden */
export type TransactionResult<TTransactionType = void> = TransactionSummary<TTransactionType> & {
gqlTransaction: GqlTransaction;
logs?: Array<unknown>;
};

function mapGqlOutputsToTxOutputs(outputs: GqlMalleableFieldsFragment['outputs']): Output[] {
return outputs.map((o) => {
const obj = 'amount' in o ? { ...o, amount: bn(o.amount) } : o;
switch (obj.type) {
case 'CoinOutput':
return { ...obj, type: OutputType.Coin } satisfies OutputCoin;
case 'ContractOutput':
return {
...obj,
type: OutputType.Contract,
inputIndex: parseInt(obj.inputIndex, 10),
} satisfies OutputContract;
case 'ChangeOutput':
return {
...obj,
type: OutputType.Change,
} satisfies OutputChange;
case 'VariableOutput':
return { ...obj, type: OutputType.Variable } satisfies OutputVariable;
case 'ContractCreated':
return {
...obj,
type: OutputType.ContractCreated,
contractId: obj.contract,
} satisfies OutputContractCreated;
default:
return assertUnreachable(obj);
}
});
}

/**
* Represents a response for a transaction.
*/
Expand All @@ -93,22 +133,25 @@ export class TransactionResponse {
/** Gas used on the transaction */
gasUsed: BN = bn(0);
/** The graphql Transaction with receipts object. */
gqlTransaction?: GqlTransaction;

private gqlTransaction?: GqlTransaction;
private request?: TransactionRequest;
private status?: GqlStatusChangeSubscription['statusChange'];
abis?: JsonAbisFromAllCalls;
/** The expected status from the getTransactionWithReceipts response */
private expectedStatus?: GqlTransactionStatusesNames;

/**
* Constructor for `TransactionResponse`.
*
* @param id - The transaction ID.
* @param tx - The transaction ID or TransactionRequest.
* @param provider - The provider.
*/
constructor(id: string, provider: Provider, abis?: JsonAbisFromAllCalls) {
this.id = id;
constructor(tx: string | TransactionRequest, provider: Provider, abis?: JsonAbisFromAllCalls) {
this.id = typeof tx === 'string' ? tx : tx.getTransactionId(provider.getChainId());

this.provider = provider;
this.abis = abis;
this.request = typeof tx === 'string' ? undefined : tx;
}

/**
Expand All @@ -129,6 +172,72 @@ export class TransactionResponse {
return response;
}

private applyMalleableSubscriptionFields<TTransactionType = void>(
transaction: Transaction<TTransactionType>
) {
const status = this.status;
if (!status) {
return;
}

// The SDK currently submits only these
const tx = transaction as Transaction<
TransactionType.Script | TransactionType.Create | TransactionType.Blob
>;

if (status.type === 'SuccessStatus' || status.type === 'FailureStatus') {
tx.inputs = tx.inputs.map((input, idx) => {
if ('txPointer' in input) {
const correspondingInput = status.transaction.inputs?.[idx] as { txPointer: string };
return {
...input,
txPointer: TxPointerCoder.decodeFromGqlScalar(correspondingInput.txPointer),
};
}
return input;
});

tx.outputs = mapGqlOutputsToTxOutputs(status.transaction.outputs);

if ('receiptsRoot' in status.transaction) {
(tx as Transaction<TransactionType.Script>).receiptsRoot = status.transaction
.receiptsRoot as string;
}
}
}

private async getTransaction<TTransactionType = void>(): Promise<{
tx: Transaction<TTransactionType>;
bytes: Uint8Array;
}> {
if (this.request) {
const tx = this.request.toTransaction() as Transaction<TTransactionType>;
this.applyMalleableSubscriptionFields(tx);
return {
tx,
bytes: this.request.toTransactionBytes(),
};
}

const gqlTransaction = this.gqlTransaction ?? (await this.fetch());
return {
tx: this.decodeTransaction(gqlTransaction) as Transaction<TTransactionType>,
bytes: arrayify(gqlTransaction.rawPayload),
};
}

private getReceipts(): TransactionResultReceipt[] {
const status = this.status ?? this.gqlTransaction?.status;

switch (status?.type) {
case 'SuccessStatus':
case 'FailureStatus':
return status.receipts.map(processGqlReceipt);
default:
return [];
}
}

/**
* Fetch the transaction with receipts from the provider.
*
Expand All @@ -146,7 +255,7 @@ export class TransactionResponse {

for await (const { statusChange } of subscription) {
if (statusChange) {
this.expectedStatus = statusChange.type;
this.status = statusChange;
break;
}
}
Expand Down Expand Up @@ -188,23 +297,8 @@ export class TransactionResponse {
async getTransactionSummary<TTransactionType = void>(
contractsAbiMap?: AbiMap
): Promise<TransactionSummary<TTransactionType>> {
let transaction = this.gqlTransaction;

if (!transaction) {
transaction = await this.fetch();
}

const decodedTransaction = this.decodeTransaction<TTransactionType>(
transaction
) as Transaction<TTransactionType>;

let txReceipts: GqlReceiptFragment[] = [];

if (transaction?.status && 'receipts' in transaction.status) {
txReceipts = transaction.status.receipts;
}

const receipts = txReceipts.map(processGqlReceipt) || [];
const { tx: transaction, bytes: transactionBytes } =
await this.getTransaction<TTransactionType>();

const { gasPerByte, gasPriceFactor, gasCosts, maxGasPerTx } = this.provider.getGasConfig();
const gasPrice = await this.provider.getLatestGasPrice();
Expand All @@ -213,10 +307,10 @@ export class TransactionResponse {

const transactionSummary = assembleTransactionSummary<TTransactionType>({
id: this.id,
receipts,
transaction: decodedTransaction,
transactionBytes: arrayify(transaction.rawPayload),
gqlTransactionStatus: transaction.status,
receipts: this.getReceipts(),
transaction,
transactionBytes,
gqlTransactionStatus: this.status ?? this.gqlTransaction?.status,
gasPerByte,
gasPriceFactor,
abiMap: contractsAbiMap,
Expand All @@ -241,6 +335,7 @@ export class TransactionResponse {
});

for await (const { statusChange } of subscription) {
this.status = statusChange;
if (statusChange.type === 'SqueezedOutStatus') {
this.unsetResourceCache();
throw new FuelError(
Expand All @@ -253,8 +348,6 @@ export class TransactionResponse {
break;
}
}

await this.fetch();
}

/**
Expand All @@ -275,7 +368,6 @@ export class TransactionResponse {
const transactionSummary = await this.getTransactionSummary<TTransactionType>(contractsAbiMap);

const transactionResult: TransactionResult<TTransactionType> = {
gqlTransaction: this.gqlTransaction as GqlTransaction,
...transactionSummary,
};

Expand All @@ -291,11 +383,12 @@ export class TransactionResponse {
transactionResult.logs = logs;
}

const { gqlTransaction, receipts } = transactionResult;
const { receipts } = transactionResult;

if (gqlTransaction.status?.type === 'FailureStatus') {
const status = this.status ?? this.gqlTransaction?.status;
if (status?.type === 'FailureStatus') {
this.unsetResourceCache();
const { reason } = gqlTransaction.status;
const { reason } = status;
throw extractTxError({
receipts,
statusReason: reason,
Expand Down
Loading

0 comments on commit 98aaafd

Please sign in to comment.