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: decode logs from TXs submitted outside BaseInvocationScope #1940

Merged
Merged
6 changes: 6 additions & 0 deletions .changeset/tame-beans-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/account": patch
"@fuel-ts/program": patch
---

feat: decode logs from TXs submitted outside `BaseInvocationScope`
11 changes: 9 additions & 2 deletions packages/account/src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
TransactionRequestInput,
CoinTransactionRequestInput,
ScriptTransactionRequest,
JsonAbisFromAllCalls,
} from './transaction-request';
import { transactionRequestify } from './transaction-request';
import type { TransactionResultReceipt } from './transaction-response';
Expand Down Expand Up @@ -609,6 +610,12 @@ export default class Provider {

const encodedTransaction = hexlify(transactionRequest.toTransactionBytes());

let abis: JsonAbisFromAllCalls | undefined;

if (transactionRequest.type === TransactionType.Script) {
abis = transactionRequest.abis;
}

if (awaitExecution) {
const subscription = this.operations.submitAndAwait({ encodedTransaction });
for await (const { submitAndAwait } of subscription) {
Expand All @@ -625,7 +632,7 @@ export default class Provider {
}

const transactionId = transactionRequest.getTransactionId(this.getChainId());
const response = new TransactionResponse(transactionId, this);
const response = new TransactionResponse(transactionId, this, abis);
await response.fetch();
return response;
}
Expand All @@ -634,7 +641,7 @@ export default class Provider {
submit: { id: transactionId },
} = await this.operations.submit({ encodedTransaction });

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

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { ContractTransactionRequestOutput, VariableTransactionRequestOutput
import { returnZeroScript } from './scripts';
import type { BaseTransactionRequestLike } from './transaction-request';
import { BaseTransactionRequest } from './transaction-request';
import type { JsonAbisFromAllCalls } from './types';

/**
* @hidden
Expand All @@ -30,6 +31,8 @@ export interface ScriptTransactionRequestLike extends BaseTransactionRequestLike
script?: BytesLike;
/** Script input data (parameters) */
scriptData?: BytesLike;

abis?: JsonAbisFromAllCalls;
}

/**
Expand All @@ -52,6 +55,8 @@ export class ScriptTransactionRequest extends BaseTransactionRequest {
/** Script input data (parameters) */
scriptData: Uint8Array;

abis?: JsonAbisFromAllCalls;

/**
* Constructor for `ScriptTransactionRequest`.
*
Expand All @@ -62,6 +67,7 @@ export class ScriptTransactionRequest extends BaseTransactionRequest {
this.gasLimit = bn(gasLimit);
this.script = arrayify(script ?? returnZeroScript.bytes);
this.scriptData = arrayify(scriptData ?? returnZeroScript.encodeScriptData());
this.abis = rest.abis;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/account/src/providers/transaction-request/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { JsonAbi } from '@fuel-ts/abi-coder';
import type { TransactionType } from '@fuel-ts/transactions';

import type {
Expand All @@ -13,3 +14,8 @@ export type TransactionRequest = ScriptTransactionRequest | CreateTransactionReq
export type TransactionRequestLike =
| ({ type: TransactionType.Script } & ScriptTransactionRequestLike)
| ({ type: TransactionType.Create } & CreateTransactionRequestLike);

export type JsonAbisFromAllCalls = {
main: JsonAbi;
otherContractsAbis: Record<string, JsonAbi>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@ export function getDecodedLogs<T = unknown>(
* These ABIs were added using `contract.addContracts` or through a multicall with `contract.multiCall`.
*
* @param receipts - The array of transaction result receipts.
* @param mainAbi - The ABI of the main contract.
* @param mainAbi - The ABI of the script or main contract.
* @param externalAbis - The record of external contract ABIs.
* @returns An array of decoded logs from Sway projects.
*/
return receipts.reduce((logs: T[], receipt) => {
if (receipt.type === ReceiptType.LogData || receipt.type === ReceiptType.Log) {
const interfaceToUse = externalAbis[receipt.id]
? new Interface(externalAbis[receipt.id])
: new Interface(mainAbi);
const interfaceToUse = new Interface(externalAbis[receipt.id] || mainAbi);

const data =
receipt.type === ReceiptType.Log
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { TransactionCoder } from '@fuel-ts/transactions';
import { arrayify } from '@fuel-ts/utils';

import type Provider from '../provider';
import type { JsonAbisFromAllCalls } from '../transaction-request';
import { assembleTransactionSummary } from '../transaction-summary/assemble-transaction-summary';
import { processGqlReceipt } from '../transaction-summary/receipt';
import type {
Expand All @@ -30,6 +31,8 @@ import type {
AbiMap,
} from '../transaction-summary/types';

import { getDecodedLogs } from './getDecodedLogs';

/** @hidden */
export type TransactionResultCallReceipt = ReceiptCall;
/** @hidden */
Expand Down Expand Up @@ -74,6 +77,7 @@ export type TransactionResultReceipt =
/** @hidden */
export type TransactionResult<TTransactionType = void> = TransactionSummary<TTransactionType> & {
gqlTransaction: GqlTransaction;
logs?: Array<unknown>;
};

/**
Expand All @@ -89,15 +93,18 @@ export class TransactionResponse {
/** The graphql Transaction with receipts object. */
gqlTransaction?: GqlTransaction;

abis?: JsonAbisFromAllCalls;

/**
* Constructor for `TransactionResponse`.
*
* @param id - The transaction ID.
* @param provider - The provider.
*/
constructor(id: string, provider: Provider) {
constructor(id: string, provider: Provider, abis?: JsonAbisFromAllCalls) {
this.id = id;
this.provider = provider;
this.abis = abis;
}

/**
Expand All @@ -108,8 +115,12 @@ export class TransactionResponse {
* @param id - The transaction ID.
* @param provider - The provider.
*/
static async create(id: string, provider: Provider): Promise<TransactionResponse> {
const response = new TransactionResponse(id, provider);
static async create(
id: string,
provider: Provider,
abis?: JsonAbisFromAllCalls
): Promise<TransactionResponse> {
const response = new TransactionResponse(id, provider, abis);
await response.fetch();
return response;
}
Expand Down Expand Up @@ -239,6 +250,16 @@ export class TransactionResponse {
...transactionSummary,
};

if (this.abis) {
const logs = getDecodedLogs(
transactionSummary.receipts,
this.abis.main,
this.abis.otherContractsAbis
);

transactionResult.logs = logs;
}

return transactionResult;
}

Expand Down
150 changes: 120 additions & 30 deletions packages/fuel-gauge/src/advanced-logging.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generateTestWallet } from '@fuel-ts/account/test-utils';
import type { Contract, Provider } from 'fuels';
import type { BN, Contract, Provider, WalletUnlocked } from 'fuels';
import { RequireRevertError, Script, ScriptResultDecoderError, bn } from 'fuels';

import { FuelGaugeProjectsEnum, getFuelGaugeForcProject } from '../test/fixtures';
Expand All @@ -14,13 +14,15 @@ let advancedLogContract: Contract;
let otherAdvancedLogContract: Contract;
let advancedLogId: string;
let otherLogId: string;
let minGasPrice: BN;

beforeAll(async () => {
advancedLogContract = await setupContract();
otherAdvancedLogContract = await setupOtherContract({ cache: false });
provider = advancedLogContract.provider;
advancedLogId = advancedLogContract.id.toB256();
otherLogId = otherAdvancedLogContract.id.toB256();
minGasPrice = provider.getGasConfig().minGasPrice;
});

/**
Expand Down Expand Up @@ -125,31 +127,17 @@ describe('Advanced Logging', () => {
]);
});

it('should properly decode all logs in a multicall with inter-contract calls', async () => {
describe('should properly decode all logs in a multicall with inter-contract calls', () => {
const setupCallTest = getSetupContract(FuelGaugeProjectsEnum.CALL_TEST_CONTRACT);
const setupConfigurable = getSetupContract(FuelGaugeProjectsEnum.CONFIGURABLE_CONTRACT);
const setupCoverage = getSetupContract(FuelGaugeProjectsEnum.COVERAGE_CONTRACT);

const callTest = await setupCallTest({ cache: false });
const configurable = await setupConfigurable({ cache: false });
const coverage = await setupCoverage({ cache: false });

let wallet: WalletUnlocked;
const testStruct = {
a: true,
b: 100000,
};

const { logs } = await callTest
.multiCall([
advancedLogContract.functions
.test_log_from_other_contract(10, otherLogId)
.addContracts([otherAdvancedLogContract]),
callTest.functions.boo(testStruct),
configurable.functions.echo_struct(),
coverage.functions.echo_str_8('fuelfuel'),
])
.call();

const expectedLogs = [
'Hello from main Contract',
'Hello from other Contract',
Expand All @@ -160,33 +148,135 @@ describe('Advanced Logging', () => {
'fuelfuel',
];

logs.forEach((log, i) => {
expect(JSON.stringify(log)).toBe(JSON.stringify(expectedLogs[i]));
beforeAll(async () => {
wallet = await generateTestWallet(provider, [[2_000]]);
});

it('when using InvacationScope', async () => {
const callTest = await setupCallTest({ cache: false });
const configurable = await setupConfigurable({ cache: false });
const coverage = await setupCoverage({ cache: false });

const { logs } = await callTest
.multiCall([
advancedLogContract.functions
.test_log_from_other_contract(10, otherLogId)
.addContracts([otherAdvancedLogContract]),
callTest.functions.boo(testStruct),
configurable.functions.echo_struct(),
coverage.functions.echo_str_8('fuelfuel'),
])
.call();

logs.forEach((log, i) => {
expect(JSON.stringify(log)).toBe(JSON.stringify(expectedLogs[i]));
});
});

it('when using ScriptTransactionRequest', async () => {
const callTest = await setupCallTest({ cache: false });
const configurable = await setupConfigurable({ cache: false });
const coverage = await setupCoverage({ cache: false });

const request = await callTest
.multiCall([
advancedLogContract.functions
.test_log_from_other_contract(10, otherLogId)
.addContracts([otherAdvancedLogContract]),
callTest.functions.boo(testStruct),
configurable.functions.echo_struct(),
coverage.functions.echo_str_8('fuelfuel'),
])
.getTransactionRequest();

const { maxFee, gasUsed, requiredQuantities } = await provider.getTransactionCost(
request,
[],
{
resourcesOwner: wallet,
}
);

request.gasLimit = gasUsed;
request.gasPrice = minGasPrice;

await wallet.fund(request, requiredQuantities, maxFee);

const tx = await wallet.sendTransaction(request);

const { logs } = await tx.waitForResult();

expect(logs).toBeDefined();

logs?.forEach((log, i) => {
if (typeof log === 'object') {
expect(JSON.stringify(log)).toBe(JSON.stringify(expectedLogs[i]));
} else {
expect(log).toBe(expectedLogs[i]);
}
});
});
});

it('should decode logs from a script set to manually call other contracts', async () => {
describe('decode logs from a script set to manually call other contracts', () => {
const { abiContents, binHexlified } = getFuelGaugeForcProject(
FuelGaugeProjectsEnum.SCRIPT_CALL_CONTRACT
);

const wallet = await generateTestWallet(provider, [[1_000]]);

const script = new Script(binHexlified, abiContents, wallet);

const amount = Math.floor(Math.random() * 10) + 1;

const { logs } = await script.functions
.main(advancedLogId, otherLogId, amount)
.addContracts([advancedLogContract, otherAdvancedLogContract])
.call();
let wallet: WalletUnlocked;

expect(logs).toStrictEqual([
const expectedLogs = [
'Hello from script',
'Hello from main Contract',
'Hello from other Contract',
'Received value from main Contract:',
amount,
]);
];

beforeAll(async () => {
wallet = await generateTestWallet(provider, [[1_000]]);
});

it('when using InvocationScope', async () => {
const script = new Script(binHexlified, abiContents, wallet);
const { logs } = await script.functions
.main(advancedLogId, otherLogId, amount)
.addContracts([advancedLogContract, otherAdvancedLogContract])
.call();

expect(logs).toStrictEqual(expectedLogs);
});

it('when using ScriptTransactionRequest', async () => {
const script = new Script(binHexlified, abiContents, wallet);

const request = await script.functions
.main(advancedLogId, otherLogId, amount)
.addContracts([advancedLogContract, otherAdvancedLogContract])
.getTransactionRequest();

const { maxFee, gasUsed, requiredQuantities } = await provider.getTransactionCost(
request,
[],
{
resourcesOwner: wallet,
}
);

request.gasLimit = gasUsed;
request.gasPrice = minGasPrice;

await wallet.fund(request, requiredQuantities, maxFee);

const tx = await wallet.sendTransaction(request);

const { logs } = await tx.waitForResult();

expect(logs).toBeDefined();

expect(logs).toStrictEqual(expectedLogs);
});
});
});
Loading
Loading