How to purchase and transfer an NFT from a contract to sp.sender?

Hi,

I’m trying to build a contract that will purchase an NFT for me from a marketplace contract (we can assume objkt.com for now) and then transfer it to sp.sender.
I manager to create a contract that will buy the NFT, but the NFT stays with the contract.

My assumption is that I need to, once purchased, transfer the NFT from the contract (the new owner after purchasing it) to sp.sender (target owner).

My question is;

  1. Is that the correct approach or is there a more elegant solution to passthrough sp.sender as the receiver of the NFT when purchasing the NFT from the marketplace?
  2. If my approach is the right one, how can I then transfer from the contract to sp.sender. I wrote the code below but I keep on running in the nftContract interface doesn't exist with it. I assume I build the params for the transfer function wrong

Here is the code snippet for the purchase and transfer:

       @sp.entrypoint
        def objktPurchase(self, objktAddress, ask_id, nftAddress, nftId, price):
            sp.cast(sp.amount, sp.mutez)
            sp.cast(price, sp.mutez)
            sp.cast(ask_id, sp.nat)
            objktContract = sp.contract(
                sp.record(ask_id = sp.nat, proxy = sp.option[sp.address]), 
                objktAddress, 
                entrypoint="fulfill_ask"
            )
            sp.transfer(
                sp.record(
                    ask_id = ask_id,
                    proxy = None,
                ),
                sp.amount - fee,
                objktContract.unwrap_some(error="objktContract interface doesn't exist"))
            
            sp.cast(nftId, sp.nat)
            sp.cast(nftAddress, sp.address)
            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= 1,
                    )], 
                )],
                sp.mutez(0),
                nftContract.unwrap_some(error="nftContract interface doesn't exist")
            )

Thank you!

is there a more elegant solution to passthrough sp.sender as the receiver of the NFT when purchasing the NFT from the marketplace?

Yes, that’s how you should to it and that’s what you did:

sp.transfer(
                [sp.record(
                    from_ = sp.self_address(),  # This is probably wrong.
                    txs = [sp.record(
                        to_= sp.sender,  # Here is the person who receives the NFT.
                        token_id= nftId, 
                        amount= 1,
                    )], 
                )],
                sp.mutez(0),
                nftContract.unwrap_some(error="nftContract interface doesn't exist")
            )

The from_ is probably wrong, in the from_ field you have to put the current owner of the NFT.
Not the person who ask for the transfer, not the receiver and not the FA2 contract.

The nftContract interface doesn't exist is another problem, I’ll answer it separately.

1 Like

when I execute that:

sp.transfer(
                sp.record(
                    ask_id = ask_id,
                    proxy = None,
                ),
                sp.amount - fee,
                objktContract.unwrap_some(error="objktContract interface doesn't exist"))

The new owner of the NFT should be the contract, no?
I can only imagine putting the from_ of the transfer as the current owner of the NFT failing, if the balance is not updated after the transfer, it would be like ‘stealing’ this NFT.
No?

I keep on running in the nftContract interface doesn't exist with it.

I see two mistakes you may have done.

  1. What is nftAddress? If it corresponds to an onchain address, the current simulator is not able to reach an onchain contract. You have to create a FA2 contract in your scenario to test it. If that’s the only error, you code may work when you originate it but not in the scenario.

  2. FA2 standard existed before the new default layout for record. As a consequence you have to specify the layout in the FA2 contact’s type. You can use the one give by the FA2 library: fa2.t.transfer_params.

from templates import fa2_lib as fa2
t = fa2.t

# In your contract
# ...
    nftContract = sp.contract(t.transfer_params, nftAddress, entrypoint="transfer")
    
# In your scenario
# ...
    scenario.add_module(fa2.t)

if the balance is not updated after the transfer

I don’t understand the question.

I’m not aware of how the objktContract works. The only thing I know is that FA2 from_ field corresponds to the person from which the transfer occurs.

I’m referring to the warning here: Operations | SmartPy

Which says that operations are done after the entrypoint is completed.
So in my flow:

  1. The contract buys the NFT
  2. The contract sends it from itself to sp.sender.

I’m not sure who has the NFT at the beginning of 2) It should be the contract as it buying the NFT.

Also, how would I construct t.transfer_params?
I wrote that:

           nftContract = sp.contract(t.transfer_params, nftAddress, entrypoint="transfer")
           nftContract.transfer([
                sp.record(
                    from_ = sp.self_address(), 
                    txs=[sp.record(token_id=nftId, amount=1, to_=sp.sender)]
                ),
            ])

Where nftId and nftAddress are params passed to my entrypoint. Based on what you shared here: Transfering FA2 between two users - #2 by Jordan

But I get the following error:

(nftContract : sp.option(sp.contract(sp.list(sp.record(from_ = sp.address, txs = sp.list(sp.record(amount = sp.nat, to_ = sp.address, token_id = sp.nat).layout(("to_", ("token_id", "amount"))))).layout(("from_", "txs")))))) has no field \'transfer\' of type sp.unknown
Expected type \'TRecord++(transfer = sp.unknown)\', but got \'sp.option(sp.contract(sp.list(sp.record(from_ = sp.address, txs = sp.list(sp.record(amount = sp.nat, to_ = sp.address, token_id = sp.nat).layout(("to_", ("token_id", "amount"))))).layout(("from_", "txs")))))\'.
(__main__, line 65)

I believe the import from the library is not working
Thanks!

I’m referring to the warning here: Operations | SmartPy
Which says that operations are done after the entrypoint is completed.
So in my flow:

  1. The contract buys the NFT
  2. The contract sends it from itself to sp.sender.

I’m not sure who has the NFT at the beginning of 2) It should be the contract as it buying the NFT.

I confirm that you don’t have any problem here, at the beginning of 2) everything from 1) has been completed.

For example, imagine you have:

sp.transfer(blah_blah1)
sp.transfer(blah_blah2)

Both sp.transfer add transfer operations. At the end of the entrypoint the transfer operation with blah_blah1 is executed so the entrypoint called by blah_blah1 runs. At the end of it, there may be transfer executed by this entrypoint and so on. When every subsequent operations are completed then blah_blah2 is executed.

Most of the time everything behave exactly like you would think without paying attention to this warning.

1 Like

This way to call an entrypoint in only valid in a scenario.
In an entrypoint you can use the sp.transfer instruction.

nftContract = sp.contract(t.transfer_params, nftAddress, entrypoint="transfer").unwrap_some()
sp.transfer([sp.record(
        from_ = sp.self_address(), 
        txs=[sp.record(token_id=nftId, amount=1, to_=sp.sender)]
    ),
], sp.tez(0), nftContract)

Sorry, took me a while to test, but just did and it all works perfectly!

Thank you again so much for your help Jordan!