Skip to content

Commit

Permalink
chore!: refactor predicate and script deployment (#3389)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedsalk authored Nov 14, 2024
1 parent 5dee7ff commit b0e9c84
Show file tree
Hide file tree
Showing 20 changed files with 327 additions and 541 deletions.
6 changes: 6 additions & 0 deletions .changeset/little-moons-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/contract": minor
"@fuel-ts/account": patch
---

chore!: refactor predicate and script deployment
28 changes: 15 additions & 13 deletions apps/docs-snippets2/src/predicates/deploying-predicates.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// #region full
import { Provider, Wallet, ContractFactory } from 'fuels';
import { Provider, Wallet } from 'fuels';

import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../env';
import { ConfigurablePin, ConfigurablePinLoader } from '../typegend/predicates';
Expand All @@ -10,20 +10,18 @@ const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const receiver = Wallet.generate({ provider });
const baseAssetId = provider.getBaseAssetId();

// We can deploy dyanmically or via `fuels deploy`
const factory = new ContractFactory(
ConfigurablePin.bytecode,
ConfigurablePin.abi,
wallet
);
const { waitForResult: waitForDeploy } =
await factory.deployAsBlobTxForScript();
// We can deploy dynamically or via `fuels deploy`
const originalPredicate = new ConfigurablePin({
provider,
});

const { waitForResult: waitForDeploy } = await originalPredicate.deploy(wallet);
await waitForDeploy();

// First, we will need to instantiate the script via it's loader bytecode.
// This can be imported from the typegen outputs that were created on `fuels deploy`.
// Then we can use the predicate as we would normally, such as overriding the configurables.
const predicate = new ConfigurablePinLoader({
const loaderPredicate = new ConfigurablePinLoader({
data: [23],
provider,
configurableConstants: {
Expand All @@ -32,17 +30,21 @@ const predicate = new ConfigurablePinLoader({
});

// Now, let's fund the predicate
const fundTx = await wallet.transfer(predicate.address, 100_000, baseAssetId);
const fundTx = await wallet.transfer(
loaderPredicate.address,
100_000,
baseAssetId
);
await fundTx.waitForResult();

// Then we'll execute the transfer and validate the predicate
const transferTx = await predicate.transfer(
const transferTx = await loaderPredicate.transfer(
receiver.address,
1000,
baseAssetId
);
const { isStatusSuccess } = await transferTx.waitForResult();
// #endregion full

console.log('Predicate defined', predicate);
console.log('Predicate defined', loaderPredicate);
console.log('Should fund predicate successfully', isStatusSuccess);
10 changes: 3 additions & 7 deletions apps/docs-snippets2/src/scripts/deploying-scripts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContractFactory, Provider, Wallet, hexlify } from 'fuels';
import { Provider, Wallet, hexlify } from 'fuels';
import { launchTestNode } from 'fuels/test-utils';

import {
Expand All @@ -16,13 +16,9 @@ const {
const providerUrl = testProvider.url;
const WALLET_PVT_KEY = hexlify(testWallet.privateKey);

const factory = new ContractFactory(
TypegenScript.bytecode,
TypegenScript.abi,
testWallet
);
const originalScript = new TypegenScript(testWallet);
const { waitForResult: waitForDeploy } =
await factory.deployAsBlobTxForScript();
await originalScript.deploy(testWallet);
await waitForDeploy();

// #region deploying-scripts
Expand Down
1 change: 1 addition & 0 deletions packages/account/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './wallet-manager';
export * from './predicate';
export * from './providers';
export * from './connectors';
export { deployScriptOrPredicate } from './utils/deployScriptOrPredicate';
66 changes: 27 additions & 39 deletions packages/account/src/predicate/predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Interface } from '@fuel-ts/abi-coder';
import { Address } from '@fuel-ts/address';
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import type { BytesLike } from '@fuel-ts/interfaces';
import { arrayify, hexlify, concat } from '@fuel-ts/utils';
import { arrayify, hexlify } from '@fuel-ts/utils';

import type { FakeResources } from '../account';
import { Account } from '../account';
Expand All @@ -23,6 +23,7 @@ import type {
TransactionRequestLike,
TransactionResponse,
} from '../providers';
import { deployScriptOrPredicate } from '../utils/deployScriptOrPredicate';

import { getPredicateRoot } from './utils';

Expand All @@ -35,16 +36,8 @@ export type PredicateParams<
abi: JsonAbi;
data?: TData;
configurableConstants?: TConfigurables;
loaderBytecode?: BytesLike;
};

function getDataOffset(binary: Uint8Array): number {
const buffer = binary.buffer.slice(binary.byteOffset + 8, binary.byteOffset + 16);
const dataView = new DataView(buffer);
const dataOffset = dataView.getBigUint64(0, false); // big-endian
return Number(dataOffset);
}

/**
* `Predicate` provides methods to populate transaction data with predicate information and sending transactions with them.
*/
Expand All @@ -55,7 +48,6 @@ export class Predicate<
bytes: Uint8Array;
predicateData: TData = [] as unknown as TData;
interface: Interface;
loaderBytecode: BytesLike = '';

/**
* Creates an instance of the Predicate class.
Expand All @@ -72,12 +64,6 @@ export class Predicate<
provider,
data,
configurableConstants,
/**
* TODO: Implement a getBytes method within the Predicate class. This method should return the loaderBytecode if it is set.
* The getBytes method should be used in all places where we use this.bytes.
* Note: Do not set loaderBytecode to a default string here; it should remain undefined when not provided.
*/
loaderBytecode = '',
}: PredicateParams<TData, TConfigurables>) {
const { predicateBytes, predicateInterface } = Predicate.processPredicateData(
bytecode,
Expand All @@ -89,7 +75,6 @@ export class Predicate<

this.bytes = predicateBytes;
this.interface = predicateInterface;
this.loaderBytecode = loaderBytecode;
if (data !== undefined && data.length > 0) {
this.predicateData = data;
}
Expand Down Expand Up @@ -243,8 +228,7 @@ export class Predicate<
private static setConfigurableConstants(
bytes: Uint8Array,
configurableConstants: { [name: string]: unknown },
abiInterface: Interface,
loaderBytecode?: BytesLike
abiInterface: Interface
) {
const mutatedBytes = bytes;

Expand All @@ -270,26 +254,6 @@ export class Predicate<

mutatedBytes.set(encoded, offset);
});

if (loaderBytecode) {
/**
* TODO: We mutate the predicate bytes here to be the loader bytes only if the configurables are being set.
* What we actually need to do here is to mutate the loader bytes to include the configurables.
*/
const offset = getDataOffset(bytes);

// update the dataSection here as necessary (with configurables)
const dataSection = mutatedBytes.slice(offset);

const dataSectionLen = dataSection.length;

// Convert dataSectionLen to big-endian bytes
const dataSectionLenBytes = new Uint8Array(8);
const dataSectionLenDataView = new DataView(dataSectionLenBytes.buffer);
dataSectionLenDataView.setBigUint64(0, BigInt(dataSectionLen), false);

mutatedBytes.set(concat([loaderBytecode, dataSectionLenBytes, dataSection]));
}
} catch (err) {
throw new FuelError(
ErrorCode.INVALID_CONFIGURABLE_CONSTANTS,
Expand Down Expand Up @@ -338,4 +302,28 @@ export class Predicate<

return index;
}

/**
*
* @param account - The account used to pay the deployment costs.
* @returns The _blobId_ and a _waitForResult_ callback that returns the deployed predicate
* once the blob deployment transaction finishes.
*
* The returned loader predicate will have the same configurable constants
* as the original predicate which was used to generate the loader predicate.
*/
async deploy<T = this>(account: Account) {
return deployScriptOrPredicate<T>({
deployer: account,
abi: this.interface.jsonAbi,
bytecode: this.bytes,
loaderInstanceCallback: (loaderBytecode, newAbi) =>
new Predicate({
bytecode: loaderBytecode,
abi: newAbi,
provider: this.provider,
data: this.predicateData,
}) as T,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { BaseTransactionRequest, TransactionType } from './transaction-request';
export interface BlobTransactionRequestLike extends BaseTransactionRequestLike {
/** Blob ID */
blobId: string;
/** Witness index of contract bytecode to create */
/** Witness index of the bytecode to create */
witnessIndex?: number;
}

Expand All @@ -25,7 +25,7 @@ export class BlobTransactionRequest extends BaseTransactionRequest {
type = TransactionType.Blob as const;
/** Blob ID */
blobId: string;
/** Witness index of contract bytecode to create */
/** Witness index of the bytecode to create */
witnessIndex: number;

/**
Expand Down
118 changes: 118 additions & 0 deletions packages/account/src/utils/deployScriptOrPredicate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { JsonAbi } from '@fuel-ts/abi-coder';
import { FuelError, ErrorCode } from '@fuel-ts/errors';
import { hash } from '@fuel-ts/hasher';
import { bn } from '@fuel-ts/math';
import { arrayify } from '@fuel-ts/utils';

import type { Account } from '../account';
import { BlobTransactionRequest, calculateGasFee, TransactionStatus } from '../providers';

import {
getDataOffset,
getPredicateScriptLoaderInstructions,
} from './predicate-script-loader-instructions';

async function fundBlobTx(deployer: Account, blobTxRequest: BlobTransactionRequest) {
// Check the account can afford to deploy all chunks and loader
let totalCost = bn(0);
const chainInfo = deployer.provider.getChain();
const gasPrice = await deployer.provider.estimateGasPrice(10);
const priceFactor = chainInfo.consensusParameters.feeParameters.gasPriceFactor;

const minGas = blobTxRequest.calculateMinGas(chainInfo);

const minFee = calculateGasFee({
gasPrice,
gas: minGas,
priceFactor,
tip: blobTxRequest.tip,
}).add(1);

totalCost = totalCost.add(minFee);

if (totalCost.gt(await deployer.getBalance())) {
throw new FuelError(ErrorCode.FUNDS_TOO_LOW, 'Insufficient balance to deploy predicate.');
}

const txCost = await deployer.getTransactionCost(blobTxRequest);
// eslint-disable-next-line no-param-reassign
blobTxRequest.maxFee = txCost.maxFee;
return deployer.fund(blobTxRequest, txCost);
}

function adjustConfigurableOffsets(jsonAbi: JsonAbi, configurableOffsetDiff: number) {
const { configurables: readOnlyConfigurables } = jsonAbi;
const configurables: JsonAbi['configurables'] = [];
readOnlyConfigurables.forEach((config) => {
// @ts-expect-error shut up the read-only thing
configurables.push({ ...config, offset: config.offset - configurableOffsetDiff });
});
return { ...jsonAbi, configurables } as JsonAbi;
}

interface Deployer<T> {
deployer: Account;
bytecode: Uint8Array;
abi: JsonAbi;
loaderInstanceCallback: (loaderBytecode: Uint8Array, newAbi: JsonAbi) => T;
}

export async function deployScriptOrPredicate<T>({
deployer,
bytecode,
abi,
loaderInstanceCallback,
}: Deployer<T>) {
const dataSectionOffset = getDataOffset(arrayify(bytecode));
const byteCodeWithoutDataSection = bytecode.slice(0, dataSectionOffset);

// Generate the associated create tx for the loader contract
const blobId = hash(byteCodeWithoutDataSection);

const blobTxRequest = new BlobTransactionRequest({
blobId,
witnessIndex: 0,
witnesses: [byteCodeWithoutDataSection],
});

const { loaderBytecode, blobOffset } = getPredicateScriptLoaderInstructions(
arrayify(bytecode),
arrayify(blobId)
);

const configurableOffsetDiff = byteCodeWithoutDataSection.length - (blobOffset || 0);
const newAbi = adjustConfigurableOffsets(abi, configurableOffsetDiff);

const blobExists = (await deployer.provider.getBlobs([blobId])).length > 0;

const loaderInstance = loaderInstanceCallback(loaderBytecode, newAbi);
if (blobExists) {
return {
waitForResult: () => Promise.resolve(loaderInstance),
blobId,
};
}

const fundedBlobRequest = await fundBlobTx(deployer, blobTxRequest);

// Transaction id is unset until we have funded the create tx, which is dependent on the blob tx
const waitForResult = async () => {
try {
const blobTx = await deployer.sendTransaction(fundedBlobRequest);
const result = await blobTx.waitForResult();

if (result.status !== TransactionStatus.success) {
throw new Error();
}
} catch (err: unknown) {
throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy predicate chunk');
}

return loaderInstance;
};

return {
waitForResult,
blobId,
};
}
Loading

0 comments on commit b0e9c84

Please sign in to comment.