Comment on page
Swap contract Demo
The swap contract is an interactive example, written in Python, that integrates data feed from a Charli3's oracle. The contract support trade operations, liquidity and minting operations.
After the Vasil upgrade, the Charli3 developer team faced the challenge of transitioning from the previous Haskell-based wallet architecture to a new architecture that fully supports Vasil's features. One important aspect of this transition was the creation of an oracle data feed as a reference input for various transactions. To achieve this, the team decided to rewrite a Haskell contract that utilizes reference inputs, leveraging the Pycardano, Python library. This approach enabled them to explore the capabilities of the library and develop a comprehensive tutorial on reading information from Charli3's oracles using contracts.
In this guide, we will demonstrate how to access the oracle feed through reference UTXOs and illustrate the process of creating transactions using Pycardano.
Before we proceed with the code, it's worth noting that while the off-chain portion of smart contracts can be rewritten in different programming languages, the on-chain code can only be written in Haskell (although there are rising alternative available). Keeping that in mind, this guide will focus on explaining the main functions of the smart contract, emphasizing the distinctions between the Haskell and Python implementations of the code.
The swap contract supports four primary operations:
- The Run swap transaction initiates the creation of a UTXO at the contract address, which includes a minted NFT. This NFT serves as an identifier for the UTXO that will hold two assets.
- The Add liquidity transaction allows the user to add specific amounts of tokens to the swap's UTXO. These token quantities must be available in the user's wallet.
- The Swap A transaction facilitates the exchange of asset A from the customers' wallet to the swap's UTXO in return for asset B.
- The Swap B transaction enables the exchange of asset B from the customers' wallet to the swap's UTXO in return for asset A.
The on-chain component of the swap contract validates that when multiplied by the oracle feed, the input tokens from the customers' wallet result in the deterministic addition or removal of assets from the swap contract's UTXO. It also adds or removes tokens from the user's wallet accordingly.
Furthermore, the contract guarantees that the "add liquidity" transaction is always an incremental operation.
Swap's validator
1
{- The validator argument contains information about the assets involved in the trade, the datum represents the unit type, the redeemer specifies the quantity of assets to be exchanged by the user, and the context provides transaction-related information.
2
-}
3
{-# INLINABLE mkSwapValidator #-}
4
mkSwapValidator
5
:: Swap
6
-> ()
7
-> SwapRedeemer
8
-> ScriptContext
9
-> Bool
10
mkSwapValidator Swap{..} _ (SwapA amountA) ctx =
11
mkSwapXValidator checkExchangeA coinB oracle amountA ctx
12
mkSwapValidator Swap{..} _ (SwapB amountB) ctx =
13
mkSwapXValidator checkExchangeB coinA oracle amountB ctx
14
mkSwapValidator swap _ AddLiquidity ctx =
15
mkAddLiqValidator swap ctx
The initial transaction, Run swap, establishes the contract's address and generates a UTXO that includes the minted SWAP NFT, serving as the pool liquidity.
The Pycardano library simplifies the setup of a Cardano development environment by integrating Blockfrost's services. To utilize these services, you need a Blockfrost account and a token ID to interact with the Cardano blockchain.
Environment's settings
1
BLOCKFROST_PROJECT_ID = "BLOCKFROST_API_PROJECT_ID"
2
BLOCKFROST_BASE_URL = "https://cardano-preprod.blockfrost.io/api"
The Start operation creates a unique SWAP NFT at a contract address. Once the SWAP UTXO is generated, it needs to be filled with assets to serve as a liquidity pool. The contract operates with a single UTXO for all transactions.
See below for the token name variable and quantity of tokens to mint.
Token Name
1
asset_name = "SWAP" #Token Name
2
nft_swap = MultiAsset.from_primitive(
3
{
4
policy_id.payload: {
5
bytes(asset_name, "utf-8"): 1, #Amount to mint
6
}
7
}
8
)
We proceed to invoke the mint function to automatically generate the UTXO containing the custom NFT.
Mint class and function
1
swap_utxo_nft = Mint(...) #Mint class (Hidden arguments)
2
swap_utxo_nft.mint_nft_with_script() #Creation of swap UTXO with NFT
Note: The example provided does not require the execution of the Start swap transaction. Its purpose is to showcase how to mint assets using Pycardano. In the actual implementation, this function is executed automatically when a new swap address is created through on-chain code.
The Add liquidity operation transfers a specified amount of the defined assets, USDT and TADA, from the user's wallets to the SWAP UTXO created earlier. This allows the contract to enable trades of assets from the UTXO with any user possessing any of the predetermined assets.
Note: To enhance the code:
- Use separate wallets for adding assets and trading with the swap contract, improving security and separation of concerns.
- Implement additional security measures like multi-signature or threshold signature schemes to ensure authorized participation in adding assets or trading.
Add liquidity class and function
1
swapInstance = SwapContract(...) #Swap contract class (Hidden args)
2
swapInstance.add_liquidity(amountA, amountB, ...) #The user's wallet associated must cover the asset amount expected by the liquidity function.
Now, we will delve into the implementation of the Swap A and Swap B transactions within the Python code. It is assumed that the creation and filling of liquidity for the SWAP UTXO have already been accomplished.
To begin, you need to provide the following information:
- 1.Oracle contract address and its UTXO feed NFT identifier.
- 2.Swap contract address and its UTXO NFT identifier.
- 3.User's wallet address.
- 4.Details of the assets being traded.
Note that in this example, tADA information is not required as it doesn't contain a policy ID or asset name.
It is assumed that a valid oracle contract already exists on the blockchain, and you can use it for testing purposes. If using a private test-net, you have the option to deploy your own oracle contract.
swap contract's arguments
1
# Charli3's oracle contract address
2
oracle_address = Address.from_primitive(
3
"addr_test1wz58xs5ygmjf9a3p6y3qzmwxp7cyj09zk90rweazvj8vwds4d703u"
4
)
5
6
# Custom contract address (swap contract)
7
swap_address = Address.from_primitive(
8
"addr_test1wqhsrhfqs6xv9g39mraau2jwnaqd7utt9x50d5sfmlz972spwd66j"
9
)
10
11
# Oracle feed nft identity
12
oracle_nft = MultiAsset.from_primitive(
13
{
14
"8fe2ef24b3cc8882f01d9246479ef6c6fc24a6950b222c206907a8be": {
15
b"InlineOracleFeed": 1
16
}
17
}
18
)
19
20
# Swap nft identity
21
swap_nft = MultiAsset.from_primitive(
22
{"ce9d1f8f464e1e930f19ae89ccab3de93d11ee5518eed15d641f6693": {b"SWAP": 1}}
23
)
24
25
# Swap asset information
26
tUSDT = MultiAsset.from_primitive(
27
{"c6f192a236596e2bbaac5900d67e9700dec7c77d9da626c98e0ab2ac": {b"USDT": 1}}
28
)
29
30
#User's wallet addressython
31
user_address = w.user_address()
The Pycardano library supports mnemonic wallets. In this example, we utilize a 24-word Shelley-compatible wallet to sign transactions by restoring an existing wallet via the wallet recovery phase.
The oracle feed data follows a standard format created with a Haskell library. Using the library's generic data type, we generate the inline oracle data. Accessing the information is as simple as reading the UTXO that holds this data as a reference UTXO.
The following on-chain Haskell code snippet demonstrates how to read the datum and extract the integer value representing the exchange price. Additionally, in this example we ensure that the oracle feed UTXO is the only input reference UTXO.
On-chain: Oracle feed reading
1
mOracleFeed :: Maybe OracleFeed
2
mOracleFeed =
3
case txInfoReferenceInputs $ scriptContextTxInfo ctx of
4
[txIn] -> case txOutDatum $ txInInfoResolved txIn of
5
NoOutputDatum -> Nothing
6
OutputDatumHash odh ->
7
case findDatum odh (scriptContextTxInfo ctx) of
8
Just dv -> PlutusTx.fromBuiltinData . getDatum $ dv
9
Nothing -> Nothing
10
OutputDatum dv ->
11
PlutusTx.fromBuiltinData . getDatum $ dv
12
[] -> traceError
13
"mkSwapXValidator: Empty reference input list"
14
_ -> traceError
15
"mkSwapXValidator: Only one reference input is allowed"
16