Call one contract from another

The following question has been asked on Telegram:

I’m trying to write a very simple contract that would have one function.
The function would be used to call another contract. I want it to be flexible enough to be able to call any contract and any address by passing as a parameter, the target_contract address, the function I want to call and to pass one or multiple parameters.
Is there some doc around something like that?

1 Like

I think you can use sp.transfer.
You can found a detailed explanation on the official Manual.

There are three concepts here: sp.transfer(param, amount, contract), address and sp.contract[t].

  • sp.contract[t] it the type for the representation of an existing contract. The value contains the entrypoint name and its type contains the entrypoint parameter’s type.

  • sp.address is in the form tzxxx, in this case it’s an implicit account, not a smart contract, or KT1xxx, in this case it’s a smart contract. It can contain an entrypoint annotation after a %, for example KT1xxx%my_entrypoint.

  • sp.contract(t, address, entrypoint) takes a defined type t, an address and optionally an entrypoint. It returns a sp.option[sp.contract[t]] that evaluates to None if no contract exists for these three parameters.

  • sp.transfer(param: t, amount: sp.tez, contract: sp.contract[t]) is the way you can call a contract. You can see that it takes a parameter of type sp.contract[t] so you can only transfer to contracts that exist and with a determined type. There is no way to dynamically set the type.

In the example below I use the sp.cast(value, type) to make clear what type is used. The type may be induced by usage, if the type is not explicitly set and cannot be induced, SmartPy will give a compilation error, most of the time the induction is sufficient, no need to explicit the type everywhere.

Here is an example demonstrating how to mix those concepts with the three possible entrypoints you can imagine to call another contract. In this example, we have two contracts: Caller and Receiver. The Caller contract has three entrypoints that demonstrate different ways to call the set_x entrypoint of the Receiver contract:

import smartpy as sp

@sp.module
def main():
    class Caller(sp.Contract):
        def __init__(self):
            pass

        @sp.entrypoint
        def call_set_x(self, address, param):
            """The entrypoint is given an address and construct the `sp.contract[sp.nat]`.
            The entrypoint is not dynamic.
            Args:
                address (sp.address): the address of the destination.
                param (sp.nat): the param that will be transfered.
            """
            sp.cast(param, sp.nat)
            contract = sp.contract(sp.nat, address, entrypoint="set_x")
            # `sp.contract(t, address, entrypoint)` returns a `sp.option[sp.contract[t]]`
            # It values `None` when there is no contract at this address with this given entrypoint
            #     and this given type.
            sp.transfer(
                param,
                sp.tez(0),
                contract.unwrap_some(error="Contract interface doesn't exist"))
        
        @sp.entrypoint
        def call_other(self, contract, param):
            """The entrypoint is given a contract.
            The entrypoint name is freely chosen by sender but the type is fixed.
            Args:
                contract (sp.contract[sp.nat]): the contract of the destination.
                param (sp.nat): the param that will be transfered.
            """
            sp.cast(param, sp.nat)
            sp.cast(contract, sp.contract[sp.nat])
            sp.transfer(param, sp.tez(0), contract)

        @sp.entrypoint
        def call_via_address(self, address, param):
            """The entrypoint is given an address supposely with the entrypoint annotation in it.]`.
            If no given entrypoint is attached, it will try to call an entrypoint named "default".
            Args:
                address (sp.address): the address of the destination is the form of KT1XXX%my_entrypoint.
                param (sp.nat): the param that will be transfered.
            """
            sp.cast(param, sp.nat)
            contract = sp.contract(sp.nat, address)
            sp.transfer(
                param,
                sp.tez(0),
                contract.unwrap_some(error="Contract interface doesn't exist"))

    class Receiver(sp.Contract):
        def __init__(self):
            self.data.x = 0

        @sp.entrypoint
        def set_x(self, x):
            sp.cast(x, sp.nat)
            self.data.x = x

if "templates" not in __name__:
    @sp.add_test(name="MyContract")
    def test():
        sc = sp.test_scenario(main)
        c1 = main.Caller()
        sc += c1
        c2 = main.Receiver()
        sc += c2
        sc.h2("Via the address")
        # Give only the address
        c1.call_set_x(address=c2.address, param=42)
        sc.verify(c2.data.x == 42)
        # Build the typed contract is the scenario
        set_x = sp.contract(sp.TNat, c2.address, entrypoint="set_x").open_some()
        # Give the typed contract
        sc.h2("Via a typed contract")
        c1.call_other(contract=set_x, param=1337)
        sc.verify(c2.data.x == 1337)
        # Give the address with the entrypoint annotation
        address_with_annotation = sp.to_address(set_x)
        sc.h2("Via an address with an entrypoint annotation")
        c1.call_via_address(address=address_with_annotation, param=404)
        sc.verify(c2.data.x == 404)
2 Likes

Is there a way to pass multiple parameters to a contract? I tried with sp.tuple and it doesn’t seem to work.
Thanks!

use sp.record:

sc.example(sp.record(parameter_1 = a, parameter_2 = b))

Would it be the same from within the contract ? Here, it’s just to send the transaction to the contract. How would I get them from within the entrypoint and redistribute them to the contract I call within the said entrypoint?

I don’t think to understand properly.
Do you wanna transfer the multiple params to an other contract?

yes.
similar to the original question. I want to be able to call an entrypoint from another contract that might require multiple params

Ok, something like this?

@sp.entrypoint
    def purchaseWithFee(self, cAddress, param, param2):
        #calculate fee
        fee = sp.split_tokens(sp.amount, 1, 100)
        #send fee
        sp.send(self.data.admin, fee)
        #send price
        c = sp.contract(sp.TRecord(param = sp.TNat, param2 = sp.TNat), cAddress, entrypoint="purchase").open_some()
        sp.transfer(sp.record(param = param, param2 = param2), (sp.amount - fee) , c)

Thank you, that works perfectly. My second parameter is actually optional, putting my solution below if that helps someone in the future (new syntax):

 c = sp.contract(
                sp.record(param1 = sp.nat, param2= sp.option[sp.address]), 
                address, 
                entrypoint="purchase"
            )
            sp.transfer(
                sp.record(
                    param1 = param_passed_to_the_entrypoint,
                    param2 = None,
                ),
                sp.amount - fee,
                contract.unwrap_some(error="Contract interface doesn't exist"))

I also just realized that the purchase call is done by contract in (i.e. sp.sender for contract c is the Caller contract. Can I make the purchase call from the wallet triggering the transaction instead? I want sp.sender in the purchase entrypoint to be Alice and not Caller.

To be as clear as possible, I need

sp.transfer(sp.record(param = param, param2 = param2), (sp.amount - fee) , c)

to be done from the wallet triggering the transaction and not the contract transferring the call.
Thanks!

Actually i don’t think is that possible.
What you can do is to add another param as sp.sender.
Also remember that all these transactions require other fees by chain, so the contract must have an sp.balance > sp.amount .

What do you mean adding another param as sp.sender ?
The contracts I am calling is not mine, so I cannot add anything there. Given a purchase (let’s assume it’s an NFT for this case), in this current model, the contract calling is purchasing with the funds of the wallet calling. That’s not normal.
Would I need to add to the contract the instruction to transfer the NFT afterwards?

I actually tried calling an NFT contract to test and added that to the end of my contract, but I keep running into an ‘nftContract interface doesn’t exist’ error.
If you have an idea of where I mess up. I’d image it’s within the params passed to the transfer function. But not sure.

nftContract = sp.contract(
                sp.list[sp.record(
                    from_ = sp.address,
                    txs = sp.list[sp.record(
                        to_= sp.address, 
                        token_id= sp.nat, 
                        amount= sp.nat
                    )], 
                )],
                nftAddress, 
                entrypoint="transfer"
            )
            sp.transfer(
                [sp.record(
                    from_ = sp.self_address(), 
                    txs = [sp.record(
                        to_= sp.sender, 
                        token_id= nftId, 
                        amount= nftAmounts,
                    )], 
                )],
                sp.mutez(0),
                nftContract.unwrap_some(error="nftContract interface doesn't exist")
            )

I didn’t understand the other contract wasn’t your.
In this case I don’t think you can forward the sp.sender.
For the ‘NFT contract’ did you create a Token Contract?

no, that wouldn’t be mine either. But that should be irrelevant as the transfer function is standard. Correct?

Yes but the transfer entrypoint requires a Token Contract. Which one are you using?

KT1FixW2uGpkMXZTC2fBvLGLttJEXVktaqbQ

This one as an example. But could be any really as the address and id is a parameter

Can you try with this one: KT1DDT97EvCDR1PYLo1umZt3RuAdJ3Yg1ruZ?
Here’s the repo: LearningTezos/contracts/TokenTransfer/SmartPy at main · TheMastro-11/LearningTezos · GitHub.
I can help you more with this.

You have to mint a Token before transfer.

Not sure I can, the fa2_lib.py lib seems to be broken with the new syntax.
SmartPy tells me I can’t import it.

Also, is that the new syntax? Trying to build it with it.That’s part of the challenge I think.
I think it just comes down to the format of the call to the NFT contract. I’m sure the error is there. But not sure where exactly

Yes is developed on old SmartPy :frowning:
I’m sorry i can’t help you

1 Like

Thanks for trying! Really appreciate it!

1 Like