Skip to content

Commit 416e9e7

Browse files
authored
feat: implement methods to serialize/deserialize TransactionResponse (#3878)
* add methods toJson and fromJson to TransactionResponse * simplify method fromJson * add TransactionResponse serialization helpers on a new file * remove fromJson and toJson methods from TransactionResponse * update connectors to use new type * using deserializeTransactionResponseJson helper * add changeset * update changeset * update serializeTransactionResponseJson * add test suite for serializeTransactionResponseJson * add transaction request to BaseTransactionRequest constructor * adjust test suite * add test group
1 parent 0a8b276 commit 416e9e7

File tree

9 files changed

+234
-17
lines changed

9 files changed

+234
-17
lines changed

.changeset/nine-games-arrive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@fuel-ts/account": patch
3+
---
4+
5+
feat: implement methods to serialize/deserialize `TransactionResponse`

packages/account/src/account.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ import type {
3131
GetBalancesResponse,
3232
Coin,
3333
TransactionCostParams,
34-
TransactionResponse,
3534
ProviderSendTxParams,
3635
TransactionSummaryJson,
3736
TransactionResult,
3837
TransactionType,
38+
TransactionResponseJson,
39+
TransactionResponse,
3940
} from './providers';
4041
import {
4142
withdrawScript,
@@ -54,6 +55,7 @@ import {
5455
} from './providers/transaction-request/helpers';
5556
import { mergeQuantities } from './providers/utils/merge-quantities';
5657
import { serializeProviderCache } from './providers/utils/serialization';
58+
import { deserializeTransactionResponseJson } from './providers/utils/transaction-response-serialization';
5759
import { AbstractAccount } from './types';
5860
import { assembleTransferToContractScript } from './utils/formatTransferToContractScriptData';
5961
import { splitCoinsIntoBatches } from './utils/split-coins-into-batches';
@@ -926,15 +928,15 @@ export class Account extends AbstractAccount implements WithAddress {
926928
transactionSummary: await this.prepareTransactionSummary(transactionRequest),
927929
};
928930

929-
const transaction: string | TransactionResponse = await this._connector.sendTransaction(
931+
const transaction: string | TransactionResponseJson = await this._connector.sendTransaction(
930932
this.address.toString(),
931933
transactionRequest,
932934
params
933935
);
934936

935937
return typeof transaction === 'string'
936938
? this.provider.getTransactionResponse(transaction)
937-
: transaction;
939+
: deserializeTransactionResponseJson(transaction);
938940
}
939941

940942
if (estimateTxDependencies) {

packages/account/src/connectors/fuel-connector.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { HashableMessage } from '@fuel-ts/hasher';
44
import { EventEmitter } from 'events';
55

66
import type { Asset } from '../assets/types';
7-
import type { TransactionRequestLike, TransactionResponse } from '../providers';
7+
import type { TransactionRequestLike, TransactionResponseJson } from '../providers';
88

99
import { FuelConnectorEventTypes } from './types';
1010
import type {
@@ -48,7 +48,7 @@ interface Connector {
4848
address: string,
4949
transaction: TransactionRequestLike,
5050
params?: FuelConnectorSendTxParams
51-
): Promise<string | TransactionResponse>;
51+
): Promise<string | TransactionResponseJson>;
5252
// #endregion fuel-connector-method-sendTransaction
5353
// #region fuel-connector-method-currentAccount
5454
currentAccount(): Promise<string | null>;
@@ -206,7 +206,7 @@ export abstract class FuelConnector extends EventEmitter implements Connector {
206206
_address: string,
207207
_transaction: TransactionRequestLike,
208208
_params?: FuelConnectorSendTxParams
209-
): Promise<string | TransactionResponse> {
209+
): Promise<string | TransactionResponseJson> {
210210
throw new FuelError(FuelError.CODES.NOT_IMPLEMENTED, 'Method not implemented.');
211211
}
212212

packages/account/src/providers/transaction-request/transaction-request.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export interface BaseTransactionRequestLike {
8888
outputs?: TransactionRequestOutput[];
8989
/** List of witnesses */
9090
witnesses?: TransactionRequestWitness[];
91+
/** The state of the transaction */
92+
flag?: TransactionStateFlag;
9193
}
9294

9395
type ToBaseTransactionResponse = Pick<
@@ -134,8 +136,6 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
134136
witnesses: TransactionRequestWitness[] = [];
135137

136138
/**
137-
* @hidden
138-
*
139139
* The current status of the transaction
140140
*/
141141
flag: TransactionStateFlag = { state: undefined, transactionId: undefined, summary: undefined };
@@ -154,6 +154,7 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
154154
inputs,
155155
outputs,
156156
witnesses,
157+
flag,
157158
}: BaseTransactionRequestLike = {}) {
158159
this.tip = tip ? bn(tip) : undefined;
159160
this.maturity = maturity && maturity > 0 ? maturity : undefined;
@@ -163,6 +164,7 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
163164
this.inputs = inputs ?? [];
164165
this.outputs = outputs ?? [];
165166
this.witnesses = witnesses ?? [];
167+
this.flag = flag ?? { state: undefined, transactionId: undefined, summary: undefined };
166168
}
167169

168170
static getPolicyMeta(req: BaseTransactionRequest) {

packages/account/src/providers/transaction-response/transaction-response.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { TransactionCoder, TxPointerCoder } from '@fuel-ts/transactions';
2323
import { arrayify } from '@fuel-ts/utils';
2424

2525
import type Provider from '../provider';
26-
import type { JsonAbisFromAllCalls, TransactionRequest } from '../transaction-request';
26+
import type { TransactionRequest, JsonAbisFromAllCalls } from '../transaction-request';
2727
import {
2828
assemblePreConfirmationTransactionSummary,
2929
assembleTransactionSummary,
@@ -36,6 +36,7 @@ import type {
3636
PreConfirmationTransactionSummary,
3737
} from '../transaction-summary/types';
3838
import { extractTxError } from '../utils';
39+
import type { ProviderCacheJson } from '../utils/serialization';
3940
import { deserializeProcessedTxOutput, deserializeReceipt } from '../utils/serialization';
4041

4142
import { type DecodedLogs, getAllDecodedLogs } from './getAllDecodedLogs';
@@ -103,6 +104,17 @@ type StatusChangeSubscription =
103104

104105
type StatusType = 'confirmation' | 'preConfirmation';
105106

107+
export type TransactionResponseJson = {
108+
id: string;
109+
providerUrl: string;
110+
abis?: JsonAbisFromAllCalls;
111+
status?: StatusChangeSubscription['statusChange'];
112+
preConfirmationStatus?: StatusChangeSubscription['statusChange'];
113+
providerCache: ProviderCacheJson;
114+
gqlTransaction?: GqlTransaction;
115+
requestJson?: string;
116+
};
117+
106118
/**
107119
* Represents a response for a transaction.
108120
*/
@@ -114,13 +126,14 @@ export class TransactionResponse {
114126
/** Gas used on the transaction */
115127
gasUsed: BN = bn(0);
116128
/** The graphql Transaction with receipts object. */
117-
private gqlTransaction?: GqlTransaction;
118-
private request?: TransactionRequest;
119-
private status?: StatusChangeSubscription['statusChange'];
129+
gqlTransaction?: GqlTransaction;
130+
request?: TransactionRequest;
131+
status?: StatusChangeSubscription['statusChange'];
120132
abis?: JsonAbisFromAllCalls;
133+
preConfirmationStatus?: StatusChangeSubscription['statusChange'];
134+
121135
private waitingForStreamData = false;
122136
private statusResolvers: Map<StatusType, (() => void)[]> = new Map();
123-
private preConfirmationStatus?: StatusChangeSubscription['statusChange'];
124137

125138
/**
126139
* Constructor for `TransactionResponse`.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { setupTestProviderAndWallets } from '../../test-utils';
2+
import { TransactionResponse } from '../transaction-response';
3+
4+
import {
5+
serializeTransactionResponseJson,
6+
deserializeTransactionResponseJson,
7+
} from './transaction-response-serialization';
8+
9+
/**
10+
* @group node
11+
* @group browser
12+
*/
13+
describe('Transaction Response Serialization', () => {
14+
it('should serialize and deserialize a transaction response correctly [W/ TX REQUEST]', async () => {
15+
using launched = await setupTestProviderAndWallets();
16+
const {
17+
provider,
18+
wallets: [wallet],
19+
} = launched;
20+
21+
const baseAssetId = await provider.getBaseAssetId();
22+
const request = await wallet.createTransfer(wallet.address, 100_000, baseAssetId, {
23+
expiration: 200,
24+
tip: 1,
25+
witnessLimit: 3000,
26+
});
27+
const response = await wallet.sendTransaction(request);
28+
await response.waitForResult();
29+
30+
// Serialize the response
31+
const serialized = await serializeTransactionResponseJson(response);
32+
33+
// Verify serialized data
34+
expect(serialized.id).toBe(response.id);
35+
expect(serialized.status).toBe(response.status);
36+
expect(serialized.abis).toBe(response.abis);
37+
expect(serialized.providerUrl).toBe(provider.url);
38+
expect(serialized.preConfirmationStatus).toBeDefined();
39+
expect(serialized.requestJson).toBeDefined();
40+
41+
// Undefined because fetch was never called
42+
expect(serialized.gqlTransaction).toBeUndefined();
43+
44+
// Deserialize the response
45+
const deserialized = deserializeTransactionResponseJson(serialized);
46+
47+
// Verify deserialized data
48+
expect(deserialized.id).toBe(response.id);
49+
expect(deserialized.status).toBe(response.status);
50+
expect(deserialized.abis).toBe(response.abis);
51+
expect(deserialized.provider.url).toBe(provider.url);
52+
expect(deserialized.gqlTransaction).toBeUndefined();
53+
expect(deserialized.preConfirmationStatus).toBeDefined();
54+
expect(deserialized.request).toBeDefined();
55+
56+
expect(request.toJSON()).toEqual(deserialized.request?.toJSON());
57+
});
58+
59+
it('should serialize and deserialize a transaction response correctly [W/O TX REQUEST]', async () => {
60+
using launched = await setupTestProviderAndWallets();
61+
const {
62+
provider,
63+
wallets: [wallet],
64+
} = launched;
65+
66+
const { id } = await wallet.transfer(wallet.address, 100_000);
67+
68+
const response = new TransactionResponse(id, provider, await provider.getChainId());
69+
await response.waitForResult();
70+
71+
// Serialize the response
72+
const serialized = await serializeTransactionResponseJson(response);
73+
74+
// Verify serialized data
75+
expect(serialized.id).toBe(response.id);
76+
expect(serialized.status).toBe(response.status);
77+
expect(serialized.abis).toBe(response.abis);
78+
expect(serialized.providerUrl).toBe(provider.url);
79+
expect(serialized.gqlTransaction).toBeDefined();
80+
81+
// Undefined because it was not provided at the constructor
82+
expect(serialized.requestJson).toBeUndefined();
83+
expect(serialized.preConfirmationStatus).toBeUndefined();
84+
85+
// Deserialize the response
86+
const deserialized = deserializeTransactionResponseJson(serialized);
87+
88+
// Verify deserialized data
89+
expect(deserialized.id).toBe(response.id);
90+
expect(deserialized.status).toBe(response.status);
91+
expect(deserialized.abis).toBe(response.abis);
92+
expect(deserialized.provider.url).toBe(provider.url);
93+
expect(deserialized.gqlTransaction).toBeDefined();
94+
95+
expect(deserialized.preConfirmationStatus).toBeUndefined();
96+
expect(deserialized.request).toBeUndefined();
97+
});
98+
99+
it('should serialize and deserialize a transaction response correctly [W/ TX REQUEST AND NEW TX RESPONSE]', async () => {
100+
using launched = await setupTestProviderAndWallets();
101+
const {
102+
provider,
103+
wallets: [wallet],
104+
} = launched;
105+
106+
const request = await wallet.createTransfer(wallet.address, 100_000);
107+
await wallet.sendTransaction(request);
108+
109+
const response = new TransactionResponse(request, provider, await provider.getChainId());
110+
await response.waitForResult();
111+
112+
// Serialize the response
113+
const serialized = await serializeTransactionResponseJson(response);
114+
115+
// Verify serialized data
116+
expect(serialized.id).toBe(response.id);
117+
expect(serialized.status).toBe(response.status);
118+
expect(serialized.abis).toBe(response.abis);
119+
expect(serialized.providerUrl).toBe(provider.url);
120+
expect(serialized.requestJson).toBeDefined();
121+
122+
// 'gqlTransaction' will be undefined because the TX request was provided
123+
expect(serialized.gqlTransaction).toBeUndefined();
124+
expect(serialized.preConfirmationStatus).toBeUndefined();
125+
126+
// Deserialize the response
127+
const deserialized = deserializeTransactionResponseJson(serialized);
128+
129+
// Verify deserialized data
130+
expect(deserialized.id).toBe(response.id);
131+
expect(deserialized.status).toBe(response.status);
132+
expect(deserialized.abis).toBe(response.abis);
133+
expect(deserialized.provider.url).toBe(provider.url);
134+
expect(deserialized.request).toBeDefined();
135+
136+
expect(deserialized.preConfirmationStatus).toBeUndefined();
137+
expect(deserialized.gqlTransaction).toBeUndefined();
138+
139+
expect(request.toJSON()).toEqual(deserialized.request?.toJSON());
140+
});
141+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import Provider from '../provider';
2+
import { transactionRequestify } from '../transaction-request';
3+
import type { TransactionResponseJson } from '../transaction-response';
4+
import { TransactionResponse } from '../transaction-response';
5+
6+
import { serializeProviderCache } from './serialization';
7+
8+
/**
9+
* NOTE: This is defined within a new file to avoid circular dependencies.
10+
*/
11+
12+
export const serializeTransactionResponseJson = async (
13+
response: TransactionResponse
14+
): Promise<TransactionResponseJson> => {
15+
const { id, status, abis, request, provider, gqlTransaction, preConfirmationStatus } = response;
16+
return {
17+
id,
18+
status,
19+
abis,
20+
requestJson: request ? JSON.stringify(request.toJSON()) : undefined,
21+
providerUrl: provider.url,
22+
providerCache: await serializeProviderCache(provider),
23+
gqlTransaction,
24+
preConfirmationStatus,
25+
};
26+
};
27+
28+
export const deserializeTransactionResponseJson = (json: TransactionResponseJson) => {
29+
const {
30+
id,
31+
abis,
32+
status,
33+
providerUrl,
34+
requestJson,
35+
providerCache,
36+
gqlTransaction,
37+
preConfirmationStatus,
38+
} = json;
39+
40+
const provider = new Provider(providerUrl, { cache: providerCache });
41+
const { chainId } = providerCache.chain.consensusParameters;
42+
43+
const response = new TransactionResponse(id, provider, Number(chainId), abis);
44+
45+
if (requestJson) {
46+
response.request = transactionRequestify(JSON.parse(requestJson));
47+
}
48+
49+
response.status = status;
50+
response.gqlTransaction = gqlTransaction;
51+
response.preConfirmationStatus = preConfirmationStatus;
52+
53+
return response;
54+
};

packages/account/test/fixtures/mocked-connector.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
Network,
1212
SelectNetworkArguments,
1313
AccountSendTxParams,
14-
TransactionResponse,
14+
TransactionResponseJson,
1515
} from '../../src';
1616
import type { Asset } from '../../src/assets/types';
1717
import { FuelConnector } from '../../src/connectors/fuel-connector';
@@ -112,7 +112,7 @@ export class MockConnector extends FuelConnector {
112112
_address: string,
113113
_transaction: TransactionRequestLike,
114114
_params: AccountSendTxParams
115-
): Promise<string | TransactionResponse> {
115+
): Promise<string | TransactionResponseJson> {
116116
const wallet = this._wallets.find((w) => w.address.toString() === _address);
117117
if (!wallet) {
118118
throw new Error('Wallet is not found!');

packages/account/test/fixtures/mocked-send-transaction-connector.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
AccountSendTxParams,
55
ScriptTransactionRequest,
66
TransactionStateFlag,
7-
TransactionResponse,
7+
TransactionResponseJson,
88
} from '../../src';
99

1010
import { MockConnector } from './mocked-connector';
@@ -14,7 +14,7 @@ export class MockSendTransactionConnector extends MockConnector {
1414
_address: string,
1515
_transaction: TransactionRequestLike,
1616
_params: AccountSendTxParams
17-
): Promise<string | TransactionResponse> {
17+
): Promise<string | TransactionResponseJson> {
1818
const wallet = this._wallets.find((w) => w.address.toString() === _address);
1919
if (!wallet) {
2020
throw new Error('Wallet is not found!');

0 commit comments

Comments
 (0)