This guide shows you how you can perform on-chain interaction with previously deployed TIP-4 token
This guide will be more complicated as compared with Tokensale implementation. It's recommended to pass it secondarily.
Fist of all, as usual, we should setup our development environment with locklift. For this smart-contracts guideline you need to include both TIP-3 and TIP-4 dependencies, because our Auction will be accepted in TIP-3 tokens. Let's explore some scheme of our contracts interaction and describe it
NFT creating is a green arrows flow, auction bids is a yellow. Let's describe a processes
User mints its own NFT via Collection contract
Then user deploys an Auction
Auction deploys its own TIP-3 TokenWallet via given TokenRoot address (familiar mechanic for you from TIP-3 Tokensale guide)
User sends minted NFT to Auction, which one implementing INftTransfer interface and accept this NFT
Another users sends TIP-3 tokens (bid) to Auction with notify = true parameter (see TIP-3 specs or TIP-3 guide)
Auction's TokenWallet send a callback to Auction, which one handle TIP-3 transfer - checks if incoming bid amount more than previous bid, and updates a leader bid address
When time is over, finishAuction function will send NFT to auction winner or old owner, if there is no bids was accepted
That's all! As you can see, the main mechanic of our interaction is a callbacks. Let's start implement our contracts. First, implement Collection and NFT contracts same as in TIP-4 quick start guide.
Collection.sol
pragma ever-solidity >=0.61.2;pragmaAbiHeader expire;pragmaAbiHeader time;pragmaAbiHeader pubkey;import'@itgold/everscale-tip/contracts/TIP4_2/TIP4_2Collection.sol';import'./Nft.sol';contractCollectionisTIP4_2Collection {/** * Errors **/uint8constant sender_is_not_owner =101;uint8constant value_is_less_than_required =102;/// _remainOnNft - the number of crystals that will remain after the entire mint /// process is completed on the Nft contractuint128 _remainOnNft =0.3 ton;constructor(TvmCell codeNft,string json ) TIP4_1Collection ( codeNft ) TIP4_2Collection ( json ) public { tvm.accept(); }functionmintNft(string json ) externalvirtual {require(msg.value > _remainOnNft +0.1 ton, value_is_less_than_required); tvm.rawReserve(0,4);uint256 id =uint256(_totalSupply); _totalSupply++; TvmCell codeNft =_buildNftCode(address(this)); TvmCell stateNft = tvm.buildStateInit({ contr: Nft, varInit: {_id: id}, code: codeNft });address nftAddr =new Nft{ stateInit: stateNft, value:0, flag:128 }( msg.sender, msg.sender, _remainOnNft, json ); emitNftCreated( id, nftAddr, msg.sender, msg.sender, msg.sender ); }}
We won't explain this code blocks because of it's already done in TIP-4 quick start
Then, let's deal with Auction contract. We'll get started from state and constructor, as usual. Do not forget to add interfaces we need.
Auction.sol
pragma ever-solidity >=0.61.2;pragmaAbiHeader expire;pragmaAbiHeader time;pragmaAbiHeader pubkey;// Interfaces we needs// This interface for transferring NFT to winnerimport"@itgold/everscale-tip/contracts/TIP4_1/interfaces/ITIP4_1NFT.sol";// This interface to accept NFT from ownerimport"@itgold/everscale-tip/contracts/TIP4_1/interfaces/INftTransfer.sol";// This interface for implementing tip-3 tokens receiving callbackimport"tip3/contracts/interfaces/IAcceptTokensTransferCallback.sol";// This interface for deploying TokenWalletimport"tip3/contracts/interfaces/ITokenRoot.sol";// This interface to return lower bidsimport"tip3/contracts/interfaces/ITokenWallet.sol";contractAuctionisINftTransfer, IAcceptTokensTransferCallback {uint256 static _nonce; // random nonce for affecting on addressaddress static _owner; // owner of auction and nftuint32public _startTime; // auction start time timestmp in secondsuint32public _endTime; // auction end time timestamp in secondsaddresspublic _nft; // nft which will be selluint128public _currentBid; // state for holding current max bidaddresspublic _currentWinner; // current max bid owneraddresspublic _tokenRoot; // this token we will receive for bidsaddresspublic _tokenWallet; // wallet for receive bidsboolpublic _nftReceived; // is auction already receive nftboolpublic _closed; // action end flagconstructor(uint32 startTime,uint32 endTime,address tokenRoot,address sendRemainingGasTo ) public { tvm.accept(); tvm.rawReserve(0.2 ever,0); _nftReceived =false; _closed =false; _startTime = startTime; _endTime = endTime; _tokenRoot = tokenRoot;// familiar wallet deploying mechanicITokenRoot(_tokenRoot).deployWallet { value:0.2 ever, flag:1, callback: Auction.onTokenWallet } (address(this),0.1 ever );// memento gas management :) sendRemainingGasTo.transfer({ value:0, flag:128, bounce:false }); }}
Remember about gas management and token wallet deploying mechanics from previous Venom In Action guide. Implement onTokenWallet callback the same way.
Ok, the next callback we need is onNftTransfer, that will be called when NFT owner send NFT to Auction address
Auction.sol
pragma ever-solidity >=0.61.2;...contractAuctionisINftTransfer, IAcceptTokensTransferCallback {...functiononNftTransfer(uint256,// id,address oldOwner,address,// newOwner,address,// oldManager,address,// newManager,address,// collection,address gasReceiver,TvmCell// payload ) overrideexternal { tvm.rawReserve(0.2 ever,0);if (oldOwner != _owner || _nftReceived) {// we should return an NFT, received from address, differenced from owner we sets in statemapping(address=> ITIP4_1NFT.CallbackParams) empty;// just operating with interfaceITIP4_1NFT(msg.sender).transfer{ value:0, flag:128, bounce:false }( oldOwner, gasReceiver, empty ); } else {// positive case: we got an NFT for selling! _nft = msg.sender; _nftReceived =true; } }...}
Great! Now we are ready to accept bids. Let's implement another callback onAcceptTokensTransfer, that our TokenWallet will call any time it got an incoming token transaction. Take attention! This is the main logic of our auction!
Auction.sol
pragma ever-solidity >=0.61.2;...contractAuctionisINftTransfer, IAcceptTokensTransferCallback {...functiononAcceptTokensTransfer(address,// tokenRoot,uint128 amount,address sender,address,// senderWallet,address remainingGasTo,TvmCell payload ) overrideexternal {require (msg.sender.value !=0&& msg.sender == _tokenWallet,101); tvm.rawReserve(0.2 ever,0);// check bid correctness:// * _nftReceived shoul be true (if not, it means that owner didn't send NFT yet)// * now must be between start and end// * received bid amount must be more than previous bid amountif ( _nftReceived && now >= _startTime && now <= _endTime && amount > _currentBid ) {// bid updatinguint128 oldBit = _currentBid;address oldWinner = _currentWinner; _currentBid = amount; _currentWinner = sender;if (oldBit >0) {// returning previous bid TvmCell empty;// REMEMBER that msg.sender is our TokenWallet! So we just call them to transfer backITokenWallet(msg.sender).transfer{value:0, flag:128}( oldBit, oldWinner,0, remainingGasTo,true, empty ); } } else {// if bid wasn't correct - we return it to sender// REMEMBER that msg.sender is our TokenWallet! So we just call them to transfer backITokenWallet(msg.sender).transfer{value:0, flag:128}( amount, sender,0, remainingGasTo,true, payload ); } }...}
That's it. How hard is that? The last thing we need - finishAuction function.
Auction.sol
pragma ever-solidity >=0.61.2;...contractAuctionisINftTransfer, IAcceptTokensTransferCallback {...// anyone can call this function!// so owner has no way to cheatfunctionfinishAuction(address sendRemainingGasTo ) public {// it can be finished only after endTime we setrequire(now >= _endTime,102);require(msg.value >=1 ever,103);// remember about gas management...and about gas constants libraries too :) tvm.rawReserve(0.2 ever,0);if (_currentBid >0) {// bid more than zero, so somebody has won! let's send NFT to winner _closed =true;mapping(address=> ITIP4_1NFT.CallbackParams) noCallbacks; TvmCell empty;ITIP4_1NFT(_nft).transfer{ value:0.1 ever, flag:1, bounce:false }( _currentWinner, sendRemainingGasTo, noCallbacks );// do not forget to send bid amount for auction owner!ITokenWallet(_tokenWallet).transfer{value:0, flag:128}( _currentBid, _owner,0.1 ever, sendRemainingGasTo,true, empty ); } else { _closed =true;// there is no bids, sad :(// returning NFT back to owner...may be next time :(mapping(address=> ITIP4_1NFT.CallbackParams) noCallbacks;ITIP4_1NFT(_nft).transfer{ value:0.1 ever, flag:1, bounce:false }( _owner, sendRemainingGasTo, noCallbacks ); } }...}
You can explore this sample (with tests and some scripts) by going to this <todo: link> repository. But we should talks about scripts we need, because this sample needs not only deploy scripts. Moving on.
We can take collection deploying script and NFT minting scripts from TIP-4 quick start. Script for auction deploying not a really hard too.
3-deploy-auction.ts
import { Address, getRandomNonce, WalletTypes } from"locklift";// you can pass this parameter by cli or get them by some file reading for example or calculate an address with locklift.provider.getExpectedAddress()
// we just hardcode it hereconstTOKEN_ROOT_ADDRESS=newAddress("0:72150b21cc717202dedfb787068970e9d78b6a7e15447f3c1695420768f9aafb")asyncfunctionmain() {constsigner= (awaitlocklift.keystore.getSigner("0"))!; // creating new account for Collection calling (or you can get already deployed by locklift.factory.accounts.addExistingAccount)
constsomeAccount=awaitlocklift.factory.accounts.addExistingAccount({ type:WalletTypes.WalletV3, publicKey:signer.publicKey });const { contract: sample,tx } =awaitlocklift.factory.deployContract({ contract:"Auction", publicKey:signer.publicKey, initParams: { _owner:someAccount.address, _nonce:getRandomNonce() }, constructorParams: { startTime: Math.floor(Date.now() / 1000) + 3600, // just for example. Of course you should put timestamp you want (in seconds)
endTime:Math.floor(Date.now() /1000) +14400, tokenRoot:TOKEN_ROOT_ADDRESS, sendRemainingGasTo:someAccount.address }, value:locklift.utils.toNano(5), });console.log(`Auction deployed at: ${sample.address.toString()}`);}main().then(() =>process.exit(0)).catch(e => {console.log(e);process.exit(1); });
The next script, that can be useful for you - sending NFT to Auction. Let's code
import { Address, toNano, WalletTypes } from"locklift";// you can pass this parameters by cli or get them by some file reading for example or calculate an address with locklift.provider.getExpectedAddress()
// we just hardcode it hereconstNFT_ADDRESS=newAddress("0:304150265fbbe8680759cb7ec98cfa598b8a109396338b2916de1684a36a7679")constAUCTION_ADDRESS=newAddress("0:94ebb201aa8e3d436fe1d1a9ecd80dbd46b44c11567cc69cbc11f8295f98dd32")asyncfunctionmain() {constsigner= (awaitlocklift.keystore.getSigner("0"))!; // creating new account for Collection calling (or you can get already deployed by locklift.factory.accounts.addExistingAccount)
constsomeAccount=awaitlocklift.factory.accounts.addExistingAccount({ type:WalletTypes.WalletV3, publicKey:signer.publicKey });// instantiate NFT contractconstnftInstance=awaitlocklift.factory.getDeployedContract("NFT",NFT_ADDRESS )// and call a transfer method to auction from ownerawaitnftInstance.methods.transfer({ to:AUCTION_ADDRESS, sendGasTo:someAccount.address,// attention! Next field is important for calling our onNftTransfer callback!// you may lose your NFT if you don't set up callback for auction here! callbacks: [[AUCTION_ADDRESS, {value:toNano(0.1), payload:""}]] }).send({ from:someAccount.address, amount:toNano(2) })console.log(`NFT has been sent`);}main().then(() =>process.exit(0)).catch(e => {console.log(e);process.exit(1); });
Pay attention on callback parameter of NFT's transfer method
This is really important step. You may lose your NFT if don't specify callback for our auction, because callback onNftTransfer won't be called. Same idea should be used by your auction participants. They should send TIP-3 tokens to Auction with notify: true parameter:
awaittokenWalletInstance.methods.transfer({ amount:1000000000,// with decimals recipient:AUCTION_ADDRESS,// because it got it's own wallet deployWalletValue:0,// we know, that auction wallet deployed already remainingGasTo:someAccount.address, notify:true,// IMPORTANT to set it "true" for onAcceptTokensTransfer to be called payload:""}).send({ from:someAccount.address, amount:toNano(2)})
All you need now is a write some tests with locklift supports. This all-in-one example with locklift environment, some simple tests and deploy scripts is available in repo.