diff --git a/.changeset/brave-actors-burn.md b/.changeset/brave-actors-burn.md new file mode 100644 index 00000000000..9a53fcbdd7c --- /dev/null +++ b/.changeset/brave-actors-burn.md @@ -0,0 +1,6 @@ +--- +"@fuel-ts/providers": patch +"@fuel-ts/transactions": patch +--- + +Add support for Mint transaction diff --git a/packages/contract/src/contracts/functions/invocation-results.ts b/packages/contract/src/contracts/functions/invocation-results.ts index 3314b60a9c5..c5243ac7917 100644 --- a/packages/contract/src/contracts/functions/invocation-results.ts +++ b/packages/contract/src/contracts/functions/invocation-results.ts @@ -50,17 +50,20 @@ export class InvocationResult { } } -export class FunctionInvocationResult extends InvocationResult { +export class FunctionInvocationResult< + T = any, + TTransactionType = void +> extends InvocationResult { readonly transactionId: string; readonly transactionResponse: TransactionResponse; - readonly transactionResult: TransactionResult; + readonly transactionResult: TransactionResult; readonly contract: Contract; readonly logs!: Array; constructor( funcScopes: InvocationScopeLike | Array, transactionResponse: TransactionResponse, - transactionResult: TransactionResult, + transactionResult: TransactionResult, contract: Contract, isMultiCall: boolean ) { @@ -72,14 +75,14 @@ export class FunctionInvocationResult extends InvocationResult { this.logs = this.getDecodedLogs(transactionResult.receipts); } - static async build( + static async build( funcScope: InvocationScopeLike | Array, transactionResponse: TransactionResponse, isMultiCall: boolean, contract: Contract ) { - const txResult = await transactionResponse.waitForResult(); - const fnResult = new FunctionInvocationResult( + const txResult = await transactionResponse.waitForResult(); + const fnResult = new FunctionInvocationResult( funcScope, transactionResponse, txResult, diff --git a/packages/fuel-gauge/src/contract.test.ts b/packages/fuel-gauge/src/contract.test.ts index 879484517fb..c905e60326d 100644 --- a/packages/fuel-gauge/src/contract.test.ts +++ b/packages/fuel-gauge/src/contract.test.ts @@ -14,7 +14,7 @@ import { FunctionInvocationResult, Wallet, } from 'fuels'; -import type { BN, TransactionRequestLike, TransactionResponse } from 'fuels'; +import type { BN, TransactionRequestLike, TransactionResponse, TransactionType } from 'fuels'; import { join } from 'path'; import abiJSON from '../test-projects/call-test-contract/out/debug/call-test-abi.json'; @@ -644,7 +644,12 @@ describe('Contract', () => { value: [resultA, resultB], transactionResult, // eslint-disable-next-line @typescript-eslint/no-explicit-any - } = await FunctionInvocationResult.build(invocationScopes, response, true, contract); + } = await FunctionInvocationResult.build( + invocationScopes, + response, + true, + contract + ); expect(transactionResult.transaction.witnesses.length).toEqual(1); expect(transactionResult.transaction.witnesses[0].data).toEqual(signedTransaction); diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 75077dac65e..1420f5ce26b 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -538,12 +538,17 @@ export default class Provider { /** * Get transaction with the given ID */ - async getTransaction(transactionId: string): Promise { + async getTransaction( + transactionId: string + ): Promise | null> { const { transaction } = await this.operations.getTransaction({ transactionId }); if (!transaction) { return null; } - return new TransactionCoder().decode(arrayify(transaction.rawPayload), 0)?.[0]; + return new TransactionCoder().decode( + arrayify(transaction.rawPayload), + 0 + )?.[0] as Transaction; } /** diff --git a/packages/providers/src/transaction-request/transaction-request.ts b/packages/providers/src/transaction-request/transaction-request.ts index 829f31b0a02..fcc90749bd8 100644 --- a/packages/providers/src/transaction-request/transaction-request.ts +++ b/packages/providers/src/transaction-request/transaction-request.ts @@ -11,7 +11,7 @@ import type { } from '@fuel-ts/interfaces'; import type { BigNumberish, BN } from '@fuel-ts/math'; import { bn } from '@fuel-ts/math'; -import type { Transaction } from '@fuel-ts/transactions'; +import type { TransactionCreate, TransactionScript } from '@fuel-ts/transactions'; import { TransactionType, TransactionCoder, @@ -147,7 +147,7 @@ abstract class BaseTransactionRequest implements BaseTransactionRequestLike { } protected getBaseTransaction(): Pick< - Transaction, + TransactionScript | TransactionCreate, keyof BaseTransactionRequestLike | 'inputsCount' | 'outputsCount' | 'witnessesCount' > { const inputs = this.inputs?.map(inputify) ?? []; @@ -166,7 +166,7 @@ abstract class BaseTransactionRequest implements BaseTransactionRequestLike { }; } - abstract toTransaction(): Transaction; + abstract toTransaction(): TransactionCreate | TransactionScript; toTransactionBytes(): Uint8Array { return new TransactionCoder().encode(this.toTransaction()); @@ -471,7 +471,7 @@ export class ScriptTransactionRequest extends BaseTransactionRequest { this.scriptData = arraifyFromUint8Array(scriptData ?? returnZeroScript.encodeScriptData()); } - toTransaction(): Transaction { + toTransaction(): TransactionScript { const script = arrayify(this.script ?? '0x'); const scriptData = arrayify(this.scriptData ?? '0x'); return { @@ -599,7 +599,7 @@ export class CreateTransactionRequest extends BaseTransactionRequest { this.storageSlots = [...(storageSlots ?? [])]; } - toTransaction(): Transaction { + toTransaction(): TransactionCreate { const baseTransaction = this.getBaseTransaction(); const bytecodeWitnessIndex = this.bytecodeWitnessIndex; const storageSlots = this.storageSlots?.map(storageSlotify) ?? []; diff --git a/packages/providers/src/transaction-response/transaction-response.ts b/packages/providers/src/transaction-response/transaction-response.ts index cd04f5c904d..4e3198905f5 100644 --- a/packages/providers/src/transaction-response/transaction-response.ts +++ b/packages/providers/src/transaction-response/transaction-response.ts @@ -51,7 +51,7 @@ export type TransactionResultReceipt = | TransactionResultScriptResultReceipt | TransactionResultMessageOutReceipt; -export type TransactionResult = { +export type TransactionResult = { status: TStatus extends 'success' ? { type: 'success'; programState: any } : { type: 'failure'; reason: any }; @@ -62,7 +62,7 @@ export type TransactionResult = { time: any; gasUsed: BN; fee: BN; - transaction: Transaction; + transaction: Transaction; }; const STATUS_POLLING_INTERVAL_MAX_MS = 5000; @@ -115,13 +115,15 @@ export class TransactionResponse { } /** Waits for transaction to succeed or fail and returns the result */ - async waitForResult(): Promise> { + async waitForResult(): Promise< + TransactionResult + > { const transaction = await this.#fetch(); const decodedTransaction = new TransactionCoder().decode( arrayify(transaction.rawPayload), 0 - )?.[0]; + )?.[0] as Transaction; switch (transaction.status?.type) { case 'SubmittedStatus': { @@ -157,7 +159,7 @@ export class TransactionResponse { }; } case 'SuccessStatus': { - const receipts = transaction.receipts!.map(processGqlReceipt); + const receipts = transaction.receipts?.map(processGqlReceipt) || []; const { gasUsed, fee } = calculateTransactionFee({ receipts, gasPrice: bn(transaction?.gasPrice), @@ -181,8 +183,8 @@ export class TransactionResponse { } /** Waits for transaction to succeed and returns the result */ - async wait(): Promise> { - const result = await this.waitForResult(); + async wait(): Promise> { + const result = await this.waitForResult(); if (result.status.type === 'failure') { throw new Error(`Transaction failed: ${result.status.reason}`); diff --git a/packages/transactions/src/coders/transaction.test.ts b/packages/transactions/src/coders/transaction.test.ts index a6cfeb95a00..c13e174be85 100644 --- a/packages/transactions/src/coders/transaction.test.ts +++ b/packages/transactions/src/coders/transaction.test.ts @@ -12,8 +12,8 @@ const U16 = 32; const U8 = 1; describe('TransactionCoder', () => { - it('Can encode TransactionScript without inputs, outputs and witnesses', () => { - const transaction: Transaction = { + it('Can encode/decode TransactionScript without inputs, outputs and witnesses', () => { + const transaction: Transaction = { type: TransactionType.Script, gasPrice: bn(U32), gasLimit: bn(U32), @@ -43,8 +43,8 @@ describe('TransactionCoder', () => { expect(JSON.stringify(decoded)).toEqual(JSON.stringify(transaction)); }); - it('Can encode TransactionScript with inputs, outputs and witnesses', () => { - const transaction: Transaction = { + it('Can encode/decode TransactionScript with inputs, outputs and witnesses', () => { + const transaction: Transaction = { type: TransactionType.Script, gasPrice: bn(U32), gasLimit: bn(U32), @@ -113,8 +113,8 @@ describe('TransactionCoder', () => { ); }); - it('Can encode TransactionCreate without inputs, outputs and witnesses', () => { - const transaction: Transaction = { + it('Can encode/decode TransactionCreate without inputs, outputs and witnesses', () => { + const transaction: Transaction = { type: TransactionType.Create, gasPrice: bn(U32), gasLimit: bn(U32), @@ -144,8 +144,8 @@ describe('TransactionCoder', () => { expect(JSON.stringify(decoded)).toEqual(JSON.stringify(transaction)); }); - it('Can encode TransactionCreate with inputs, outputs and witnesses', () => { - const transaction: Transaction = { + it('Can encode/decode TransactionCreate with inputs, outputs and witnesses', () => { + const transaction: Transaction = { type: TransactionType.Create, gasPrice: bn(U32), gasLimit: bn(U32), @@ -234,4 +234,65 @@ describe('TransactionCoder', () => { JSON.parse(JSON.stringify(transaction)) ); }); + + it('Can encode/decode TransactionMint with outputs', () => { + const transaction: Transaction = { + type: TransactionType.Mint, + outputsCount: 2, + outputs: [ + { + type: OutputType.Coin, + to: B256, + amount: bn(1), + assetId: B256, + }, + { + type: OutputType.Coin, + to: B256, + amount: bn(1), + assetId: B256, + }, + ], + txPointer: { + blockHeight: 0, + txIndex: 0, + }, + }; + + const encoded = hexlify(new TransactionCoder().encode(transaction)); + + expect(encoded).toEqual( + '0x000000000000000200000000000000020000000000000000d5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b0000000000000001d5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b0000000000000000d5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b0000000000000001d5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b00000000000000000000000000000000' + ); + + const [decoded, offset] = new TransactionCoder().decode(arrayify(encoded), 0); + + expect(offset).toEqual((encoded.length - 2) / 2); + expect(JSON.parse(JSON.stringify(decoded))).toMatchObject( + JSON.parse(JSON.stringify(transaction)) + ); + }); + + it('Can encode/decode TransactionMint without outputs', () => { + const transaction: Transaction = { + type: TransactionType.Mint, + outputsCount: 0, + outputs: [], + txPointer: { + blockHeight: 0, + txIndex: 0, + }, + }; + + const encoded = hexlify(new TransactionCoder().encode(transaction)); + + expect(encoded).toEqual('0x0000000000000002000000000000000000000000000000000000000000000000'); + + const [decoded, offset] = new TransactionCoder().decode(arrayify(encoded), 0); + + expect(offset).toEqual((encoded.length - 2) / 2); + expect(JSON.parse(JSON.stringify(decoded))).toMatchObject( + JSON.parse(JSON.stringify(transaction)) + ); + }); }); diff --git a/packages/transactions/src/coders/transaction.ts b/packages/transactions/src/coders/transaction.ts index 97915bd2863..88f0ab33f86 100644 --- a/packages/transactions/src/coders/transaction.ts +++ b/packages/transactions/src/coders/transaction.ts @@ -11,12 +11,15 @@ import type { Output } from './output'; import { OutputCoder } from './output'; import { StorageSlotCoder } from './storage-slot'; import type { StorageSlot } from './storage-slot'; +import type { TxPointer } from './tx-pointer'; +import { TxPointerCoder } from './tx-pointer'; import type { Witness } from './witness'; import { WitnessCoder } from './witness'; export enum TransactionType /* u8 */ { Script = 0, Create = 1, + Mint = 2, } export type TransactionScript = { @@ -291,7 +294,62 @@ export class TransactionCreateCoder extends Coder { + constructor() { + super('TransactionMint', 'struct TransactionMint', 0); + } + + encode(value: TransactionMint): Uint8Array { + const parts: Uint8Array[] = []; + + parts.push(new NumberCoder('u8').encode(value.outputsCount)); + parts.push(new ArrayCoder(new OutputCoder(), value.outputsCount).encode(value.outputs)); + parts.push(new TxPointerCoder().encode(value.txPointer)); + + return concat(parts); + } + + decode(data: Uint8Array, offset: number): [TransactionMint, number] { + let decoded; + let o = offset; + + [decoded, o] = new NumberCoder('u8').decode(data, o); + const outputsCount = decoded; + [decoded, o] = new ArrayCoder(new OutputCoder(), outputsCount).decode(data, o); + const outputs = decoded; + [decoded, o] = new TxPointerCoder().decode(data, o); + const txPointer = decoded; + + return [ + { + type: TransactionType.Mint, + outputsCount, + outputs, + txPointer, + }, + o, + ]; + } +} +type PossibleTransactions = TransactionScript | TransactionCreate | TransactionMint; +export type Transaction = TTransactionType extends TransactionType + ? Extract + : (Partial | Partial | Partial) & { + type: TransactionType; + }; export class TransactionCoder extends Coder { constructor() { @@ -304,11 +362,19 @@ export class TransactionCoder extends Coder { parts.push(new NumberCoder('u8').encode(value.type)); switch (value.type) { case TransactionType.Script: { - parts.push(new TransactionScriptCoder().encode(value)); + parts.push( + new TransactionScriptCoder().encode(value as Transaction) + ); break; } case TransactionType.Create: { - parts.push(new TransactionCreateCoder().encode(value)); + parts.push( + new TransactionCreateCoder().encode(value as Transaction) + ); + break; + } + case TransactionType.Mint: { + parts.push(new TransactionMintCoder().encode(value as Transaction)); break; } default: { @@ -325,6 +391,7 @@ export class TransactionCoder extends Coder { [decoded, o] = new NumberCoder('u8').decode(data, o); const type = decoded as TransactionType; + switch (type) { case TransactionType.Script: { [decoded, o] = new TransactionScriptCoder().decode(data, o); @@ -334,8 +401,12 @@ export class TransactionCoder extends Coder { [decoded, o] = new TransactionCreateCoder().decode(data, o); return [decoded, o]; } + case TransactionType.Mint: { + [decoded, o] = new TransactionMintCoder().decode(data, o); + return [decoded, o]; + } default: { - throw new Error('Invalid Input type'); + throw new Error('Invalid Transaction type'); } } }