Passing variables to create_contract

In the documentation it is said that create_contract takes in the contract class as the first argument and storage as the last.

sp.create_contract(sp.Contract, delegate: sp.option[sp.public_key_hash], amount: sp.mutez, storage: t) → sp.address

How should values to be used at deployed contracts init be passed on?

do you?
sp.create_contract(ExampleContract(owner=owner, balance=10), None, sp.tez(0), ())
or
sp.create_contract(ExampleContract, None, sp.tez(0), (owner=owner, balance=10))

The create_contract function doesn’t actually invoke the constructor of the provided contract. Instead, it initializes the contract’s storage directly. Therefore, when using create_contract, you’re required to specify the storage structure directly rather than providing arguments for a constructor.

With your example, you’d write:

sp.create_contract(
    ExampleContract, None, sp.tez(0), sp.record(owner=owner, balance=10))

Assuming the storage type of ExampleContract is sp.record(owner=sp.address, balance=sp.int).

Here is a more detailed example showing that we provide the storage values and not the constructor parameters.

import smartpy as sp

@sp.module
def main():
    class MyContract(sp.Contract):
        def __init__(self, b):
            sp.cast(b, sp.int)
            self.data.b = b
            self.data.c = 42  # Initializing 'c' directly

    class Factory(sp.Contract):
        def __init__(self):
            self.data.created = sp.cast(None, sp.option[sp.address])

        @sp.entrypoint
        def create_contract(self):
            self.data.created = sp.Some(sp.create_contract(
                # Here we provide c=1337, even though 'c'
                # isn't a parameter in MyContract's constructor
                MyContract, None, sp.mutez(0), sp.record(b=10, c=1337)
            ))

if "templates" not in __name__:
    @sp.add_test(name="MyContract")
    def test():
        sc = sp.test_scenario(main)
        c1 = main.MyContract(10)
        sc += c1
        factory = main.Factory()
        sc += factory
        factory.create_contract()
1 Like

Thanks for example Jordan. Really helpful.

Just to clarify. The deployed contract’s storage does not contain a record, per se. My example was about passing in more than one value. For example, if the storage would be

class MyContract(sp.Contract):
def init(self, admin, first_owner)
self.data.admin = sp.cast(admin, sp.address)
self.data.balances = sp.cast(first_owner, sp.record(sp.address, sp.nat))

Sry, not sure if my syntax is correct, but the idea is that, there is one value for admin address, and another for more complex type, like record.

How would one pass in these values in create_contract?

Whenever you define multiple fields within data, it inherently behaves as a record. In the provided example, even though there isn’t a direct record definition in the constructor, data is still structured as a record due to its multiple fields. It’s the inherent nature of SmartPy contracts to treat data with multiple fields as a record.

How would one pass in these values in create_contract?

balances seems to be more a map or a big map as explained here: How do I decide between using map, big_map, and record?.

Have a look at my first code

Now if balances is a big map:

sp.create_contract(
    ExampleContract, None, sp.tez(0), sp.record(owner=owner, balances=sp.big_map()))

Yes, you are correct, balances is not a record.

Thanks for the answer. So basically you pass in as if the storage were a record, and if there were to be a map or anything else more complex, you would pass it in as it were one element of a record.

For the sake of the example

class MyContract(sp.Contract):
	def __init__(self, admin, first_owner):
		self.data.admin = sp.cast(admin, sp.address)
		self.data.balances = sp.cast(first_owner, sp.map(address=sp.address, balance=sp.nat))

one would call

sp.create_contract(
    MyContract, 
    None, 
    sp.tez(0), 
    sp.record(admin=admin, balances[first_owner.address]=first_owner.balance))

assuming

first_owner = sp.record(address=sp.address, balance=sp.nat)?

Correct?

or would, in this case, the first_owner have to be a map object with address and balance as the first entry of the map, since constructor of MyContract takes in the ‘first_owner’ as singular parameter?

You’re close! But let’s clarify a few things about initializing maps in SmartPy.

The sp.map type is a key-value store. In your example, it looks like address would be the key and balance the associated value.

For your contract:

class MyContract(sp.Contract):
    def __init__(self, admin, first_owner):
        # Specify the type of the parameters
        sp.cast(admin, sp.address)
        sp.cast(
            first_owner,
            sp.record(address=sp.address, balance=sp.nat))
        
        # Build the balances map
        # Notice you could accept balances directly as the parameter
        # instead of building it in the constructor from a record. 
        balances = { first_owner.address: first_owner.balance }      

        # storage initialization
        self.data.admin = admin
        self.data.balances = balances

        # Assigning to self.data is a convenient way to build a record.
        # So the two previous assignation are perfectly equivalent to
        self.data = sp.record(admin=admin, balances=balances)

When using create_contract, you never invoke the constructor so you have to build the storage exactly like the constructor would do, except this time you cannot insert values into self.data.

data = sp.record(admin=admin, balances={ first_owner.address: first_owner.balance })
sp.create_contract(MyContract, None, tez(0), data)

Remember, the constructor isn’t invoked in sp.create_contract, so you’re directly setting the storage structure.

Another way to view it is to consider create_contract is giving the storage to the following constructor:

def __init__(self, data):
    self.data = data
1 Like

Thanks a lot! This is much appreciated!

As per your last response, does it mean that I also have to pass something in for the storage values that I am initializing as empty?

For example

class MyContract(sp.Contract):
    def __init__(self, idNum, totalAmount, admin):
        self.data.idNum = sp.cast(idNum, sp.nat)
        self.data.totalAmount = sp.cast(totalAmount, sp.nat)
        self.data.admin = sp.cast(admin, sp.address)

        self.data.ownersMap = sp.cast( {}, sp.map[sp.address, sp.nat])
        self.data.ticket = sp.cast(None, sp.option[sp.ticket[sp.nat]])

class ContractFactory(sp.Contract):
    ...

    @sp.entrypoint
    def createContract(self, idNum, totalAmount, admin):
        new_contr = sp.create_contract(
            MyContract, 
            None, sp.tez(0), 
            sp.record(
                idNum=idNum, 
                totalAmount=totalAmount, 
                admin=admin,
                ##  ownersMap & ticket here?
                ownersMap= {},  # seems to accept this
                ticket= ?              # but what to pass here?
                )
            )
        contractStorage = self.data.deployedContracts
        contractStorage.push(new_contr)

And how would you pass them in since they are intended to be empty initially?

rn, if I try to run the code (without passing the ownersMap or ticket), it says

Variable ‘storage’ of type TRecord++(admin = sp.address, idNum= sp.nat, totalAmount= sp.nat, ownersMap = sp.map(sp.address, sp.nat), ticket = sp.option(sp.ticket(sp.nat))) cannot be used twice because it contains a ticket.

Yes you need to provide a value for every data fields.
For ticket you can provide None.

Your error is related to something more complex. Let’s talk on Telegram.

When substituting ticket for a record of in the init method of the deployable contract as below

self.data.ticketRec = sp.cast(
                None, sp.record(
                    contract=sp.address, 
                    id=sp.nat, 
                    amount=sp.nat
                )
            )

How do you pass an empty record into create_contract? empty map seemed easy {}, but an empty record?

In SmartPy, there isn’t a concept of an “empty record” in the way that you might think of an empty map ({}). Instead, if you want a record that might be unset or “empty”, you would utilize the option type.

Refer back to the option documentation page for a clearer understanding. With SmartPy’s option system, you can define values that can be None or sp.Some<value>. The type of this is denoted as sp.option[<type of the value>].

In the code you’ve provided:

self.data.ticketRec = sp.cast(
                None, sp.record(
                    contract=sp.address, 
                    id=sp.nat, 
                    amount=sp.nat
                )
            )

You seem to be trying to cast None into a record. What you likely intend is to cast it into an option of a record. The correct way would be:
sp.cast(None, sp.option[sp.record(contract=sp.address, id=sp.nat, amount=sp.nat)])

In your example, when you use sp.cast(None, ...), you’re essentially defining an “empty” (in fact unset) record. The None in the sp.cast is already signifying the absence of a value. So, you’ve correctly represented an “unset” or “empty” record without perhaps realizing it. The explicit type definition that follows just ensures that it’s cast correctly and enforces type.