use std::collections::HashSet;
use fuel_asm::{op, Instruction, RegId};
use fuel_tx::{Bytes32, ContractId, Salt, StorageSlot};
use fuels_accounts::Account;
use fuels_core::{
constants::WORD_SIZE,
types::{
bech32::Bech32ContractId,
errors::{error, Result},
transaction::TxPolicies,
transaction_builders::{Blob, BlobId, BlobTransactionBuilder, TransactionBuilder},
},
};
use super::{compute_contract_id_and_state_root, Contract, Regular};
pub fn loader_contract_asm(blob_ids: &[BlobId]) -> Result<Vec<u8>> {
const BLOB_ID_SIZE: u16 = 32;
let get_instructions = |num_of_instructions, num_of_blobs| {
[
op::move_(0x10, RegId::PC),
op::addi(0x10, 0x10, num_of_instructions * Instruction::SIZE as u16),
op::move_(0x16, RegId::SP),
op::movi(0x13, num_of_blobs),
op::bsiz(0x11, 0x10),
op::ldc(0x10, 0, 0x11, 1),
op::addi(0x10, 0x10, BLOB_ID_SIZE),
op::subi(0x13, 0x13, 1),
op::jnzb(0x13, RegId::ZERO, 3),
op::sub(0x16, 0x16, RegId::IS),
op::divi(0x16, 0x16, 4),
op::jmp(0x16),
]
};
let num_of_instructions = u16::try_from(get_instructions(0, 0).len())
.expect("to never have more than u16::MAX instructions");
let num_of_blobs = u32::try_from(blob_ids.len()).map_err(|_| {
error!(
Other,
"the number of blobs ({}) exceeds the maximum number of blobs supported: {}",
blob_ids.len(),
u32::MAX
)
})?;
let instruction_bytes = get_instructions(num_of_instructions, num_of_blobs)
.into_iter()
.flat_map(|instruction| instruction.to_bytes());
let blob_bytes = blob_ids.iter().flatten().copied();
Ok(instruction_bytes.chain(blob_bytes).collect())
}
#[derive(Debug, Clone)]
pub struct BlobsUploaded {
blob_ids: Vec<BlobId>,
}
#[derive(Debug, Clone)]
pub struct BlobsNotUploaded {
blobs: Vec<Blob>,
}
#[derive(Debug, Clone)]
pub struct Loader<Blobs> {
as_blobs: Blobs,
}
impl Contract<Loader<BlobsNotUploaded>> {
pub fn code(&self) -> Vec<u8> {
let ids: Vec<_> = self.blob_ids();
loader_contract_asm(&ids)
.expect("a contract to be creatable due to the check done in loader_from_blobs")
}
pub fn contract_id(&self) -> ContractId {
self.compute_roots().0
}
pub fn code_root(&self) -> Bytes32 {
self.compute_roots().1
}
pub fn state_root(&self) -> Bytes32 {
self.compute_roots().2
}
fn compute_roots(&self) -> (ContractId, Bytes32, Bytes32) {
compute_contract_id_and_state_root(&self.code(), &self.salt, &self.storage_slots)
}
pub fn loader_from_blobs(
blobs: Vec<Blob>,
salt: Salt,
storage_slots: Vec<StorageSlot>,
) -> Result<Self> {
if blobs.is_empty() {
return Err(error!(Other, "must provide at least one blob"));
}
let idx_of_last_blob = blobs.len().saturating_sub(1);
let idx_of_offender = blobs.iter().enumerate().find_map(|(idx, blob)| {
(blob.len() % WORD_SIZE != 0 && idx != idx_of_last_blob).then_some(idx)
});
if let Some(idx) = idx_of_offender {
return Err(error!(
Other,
"blob {}/{} has a size of {} bytes, which is not a multiple of {WORD_SIZE}",
idx.saturating_add(1),
blobs.len(),
blobs[idx].len()
));
}
let ids = blobs.iter().map(|blob| blob.id()).collect::<Vec<_>>();
loader_contract_asm(&ids)?;
Ok(Self {
code: Loader {
as_blobs: BlobsNotUploaded { blobs },
},
salt,
storage_slots,
})
}
pub fn blobs(&self) -> &[Blob] {
self.code.as_blobs.blobs.as_slice()
}
pub fn blob_ids(&self) -> Vec<BlobId> {
self.code
.as_blobs
.blobs
.iter()
.map(|blob| blob.id())
.collect()
}
pub async fn upload_blobs(
self,
account: &impl Account,
tx_policies: TxPolicies,
) -> Result<Contract<Loader<BlobsUploaded>>> {
let provider = account.try_provider()?;
let all_blob_ids = self.blob_ids();
let mut already_uploaded = HashSet::new();
for blob in self.code.as_blobs.blobs {
let id = blob.id();
if already_uploaded.contains(&id) {
continue;
}
let mut tb = BlobTransactionBuilder::default()
.with_blob(blob)
.with_tx_policies(tx_policies)
.with_max_fee_estimation_tolerance(0.05);
account.adjust_for_fee(&mut tb, 0).await?;
account.add_witnesses(&mut tb)?;
let tx = tb.build(provider).await?;
let tx_status_response = provider.send_transaction_and_await_commit(tx).await;
match tx_status_response {
Ok(tx_status_response) => {
tx_status_response.check(None)?;
}
Err(err) => {
if !err
.to_string()
.contains("Execution error: BlobIdAlreadyUploaded")
{
return Err(err);
}
}
}
already_uploaded.insert(id);
}
Contract::loader_from_blob_ids(all_blob_ids, self.salt, self.storage_slots)
}
pub async fn deploy(
self,
account: &impl Account,
tx_policies: TxPolicies,
) -> Result<Bech32ContractId> {
self.upload_blobs(account, tx_policies)
.await?
.deploy(account, tx_policies)
.await
}
pub async fn deploy_if_not_exists(
self,
account: &impl Account,
tx_policies: TxPolicies,
) -> Result<Bech32ContractId> {
self.upload_blobs(account, tx_policies)
.await?
.deploy_if_not_exists(account, tx_policies)
.await
}
pub fn revert_to_regular(self) -> Contract<Regular> {
let code = self
.code
.as_blobs
.blobs
.into_iter()
.flat_map(Vec::from)
.collect();
Contract::regular(code, self.salt, self.storage_slots)
}
}
impl Contract<Loader<BlobsUploaded>> {
pub fn code(&self) -> Vec<u8> {
loader_contract_asm(&self.code.as_blobs.blob_ids)
.expect("a contract to be creatable due to the check done in loader_for_blobs")
}
pub fn contract_id(&self) -> ContractId {
self.compute_roots().0
}
pub fn code_root(&self) -> Bytes32 {
self.compute_roots().1
}
pub fn state_root(&self) -> Bytes32 {
self.compute_roots().2
}
pub fn compute_roots(&self) -> (ContractId, Bytes32, Bytes32) {
compute_contract_id_and_state_root(&self.code(), &self.salt, &self.storage_slots)
}
pub fn loader_from_blob_ids(
blob_ids: Vec<BlobId>,
salt: Salt,
storage_slots: Vec<StorageSlot>,
) -> Result<Self> {
if blob_ids.is_empty() {
return Err(error!(Other, "must provide at least one blob"));
}
loader_contract_asm(&blob_ids)?;
Ok(Self {
code: Loader {
as_blobs: BlobsUploaded { blob_ids },
},
salt,
storage_slots,
})
}
pub fn blob_ids(&self) -> &[BlobId] {
&self.code.as_blobs.blob_ids
}
pub async fn deploy(
self,
account: &impl Account,
tx_policies: TxPolicies,
) -> Result<Bech32ContractId> {
Contract::regular(self.code(), self.salt, self.storage_slots)
.deploy(account, tx_policies)
.await
}
pub async fn deploy_if_not_exists(
self,
account: &impl Account,
tx_policies: TxPolicies,
) -> Result<Bech32ContractId> {
Contract::regular(self.code(), self.salt, self.storage_slots)
.deploy_if_not_exists(account, tx_policies)
.await
}
}