NOTE: We are using Glittr Devnet for this tutorial.
Creating and using AMM contract
Step 1: Create the First Token
First, we create the first asset using a MOA (Mintable Only Asset) contract, for simplicity, the ticker for this asset would be "FIRST". Make a new file named 1-create-1st-asset.tsand copy the file below, change the apiKeyandwif to your values.
NOTE: When you try this, you want to change the ticker to something more unique, or you can leave it as undefined.
This file will create a simple free mint asset, broadcast it to bitcoin node (devnet), and then wait for the tx to be mined, ~1 minute in devnet. Run the script.
yarn tsx 1-create-1st-asset.ts
Result:
$ yarn tsx 1-create-1st-asset.ts
yarn run v1.22.22
$ /Users/Projects/amm-example/node_modules/.bin/tsx 1-create-1st-asset.ts
bcrt1pvt3uzd3effh222v6wr8kx0sqhj07743lu0w9e7m74urqtgd85mgq6gwtes
TXID : 3a0bc10807ccc76d06058fd76d656feef435a033126b210571ca5d0a345cfe95
[+] Waiting to be mined
Mined! Response {"block_tx":"248419:1","is_valid":true,"message":{"flaw":null,"message":{"contract_creation":{"contract_type":{"moa":{"divisibility":18,"live_time":0,"mint_mechanism":{"free_mint":{"amount_per_mint":"100000"}},"ticker":"FIRST"}},"spec":null}}}}
✨ Done in 64.86s.
The first contractId is [248419, 1] with the format of [block_height, tx_order]. Please take note of this somewhere, we will use the contractId to mint.
Step 2: Create the Second Token
Similarly, create the second token (ticker is SECOND), copy the file from the 1st step, and name it 2-create-2nd-asset.ts . Change the ticker from "FIRST" to "SECOND", and leave everything else.
NOTE: When you try this, you want to change the ticker to something more unique, or you can leave it as undefined.
$ yarn tsx 2-create-2nd-asset.ts
yarn run v1.22.22
$ /Users/Projects/amm-example/node_modules/.bin/tsx 2-create-2nd-asset.ts
bcrt1pvt3uzd3effh222v6wr8kx0sqhj07743lu0w9e7m74urqtgd85mgq6gwtes
TXID : 6cf8510b7b0166575970b134185354d412f05ccc54a37f89fe1bd7049a5bedf5
[+] Waiting to be mined
Mined! Response {"block_tx":"248424:1","is_valid":true,"message":{"flaw":null,"message":{"contract_creation":{"contract_type":{"moa":{"divisibility":18,"live_time":0,"mint_mechanism":{"free_mint":{"amount_per_mint":"100000"}},"ticker":"SECOND"}},"spec":null}}}}
✨ Done in 51.30s.
The second contractId is [248424, 1], note this somewhere because we will use this information to mint.
Step 3: Mint Both Tokens
After creating the tokens, you need to mint them. This is done in two separate steps for each token. Create a new file named 3-mint-1st-asset.tsand copy the file below. Change the apiKey, wifand also contract. The contractis your contract's BlockTx from step 1.
$ yarn tsx 3-mint-1st-asset.ts
yarn run v1.22.22
$ /Users/Projects/amm-example/node_modules/.bin/tsx 3-mint-1st-asset.ts
TXID : 19d61d826112da7d139fe0dd426d597482511de741d4285d313a6a87b1ad7bd4
[+] Waiting to be mined
Mined! Response {"block_tx":"248427:1","is_valid":true,"message":{"flaw":null,"message":{"contract_call":{"call_type":{"mint":{"pointer":1}},"contract":[248419,1]}}}}
✨ Done in 55.75s.
Do the same with the second contract, copy the file into 4-mint-2nd-asset.ts , and then change the contract's BlockTx to the second contractId. Run the new script.
yarn tsx 4-mint-2nd-asset.ts
Result:
$ yarn tsx 4-mint-2nd-asset.t
yarn run v1.22.22
$ /Users/Projects/amm-example/node_modules/.bin/tsx 4-mint-2nd-asset.t
TXID : 4bf339705da283cc5a98a32040852ac36e30e9267b84906a56a61a1fa4a1adf7
[+] Waiting to be mined
Mined! Response {"block_tx":"248428:1","is_valid":true,"message":{"flaw":null,"message":{"contract_call":{"call_type":{"mint":{"pointer":1}},"contract":[248424,1]}}}}
✨ Done in 13.32s.
You'll now have two UTXOs holding the corresponding assets, to look into your list of assets, you can refer to the Helper API. The output below is the list of address' UTXOs (format is TXID:VOUT) that hold assets, you can see that there are two UTXOs for each asset.
Now create the AMM contract that will manage the token pair, copy the file below into a new file 5-create-amm-asset.ts . Change the apiKey, wifand also the two contract.
This script will create a new asset, this asset is a collateralized asset with two input assets. Setting this asset with a proportional mint structure as "constant_product", is the same as creating AMM contract.
Run the script
yarn tsx 5-create-amm-asset.ts
Result:
$ yarn tsx 5-create-amm-asset.ts
yarn run v1.22.22
$ /Users/irfi/Projects/amm-example/node_modules/.bin/tsx 5-create-amm-asset.ts
TXID : 27342cef9918076b0c5888f61ceaaddf2703fafad29adcfffe4c951c4e3ad441
[+] Waiting to be mined
Mined! Response {"block_tx":"248437:1","is_valid":true,"message":{"flaw":null,"message":{"contract_creation":{"contract_type":{"mba":{"burn_mechanism":{},"divisibility":18,"live_time":0,"mint_mechanism":{"collateralized":{"_mutable_assets":false,"input_assets":[{"glittr_asset":[248419,1]},{"glittr_asset":[248424,1]}],"mint_structure":{"proportional":{"inital_mint_pointer_to_key":null,"ratio_model":"constant_product"}}}},"swap_mechanism":{},"ticker":"AMM"}},"spec":null}}}}
✨ Done in 4.55s.
Again, please take note somewhere the contractId for the AMM contract, you can find this on block_txfield, for this example the contractId is [248437, 1].
This creates an AMM with:
Ticker: AMM (or whatever you change)
Uses constant product formula (x y = k)
Accepts both FIRST and SECOND tokens as collateral
Step 5: Add Liquidity
To enable trading, add initial liquidity to the AMM. Copy the file below into a file 6-deposit-liquidity-amm.ts. Change the apiKey, wifand also the contract.
6-deposit-liquidity-amm.ts
import {
Account,
GlittrSDK,
Output,
OpReturnMessage,
BlockTxTuple,
electrumFetchNonGlittrUtxos,
BitcoinUTXO,
txBuilder,
addFeeToTx,
} from "@glittr-sdk/sdk";
const NETWORK = "regtest";
const client = new GlittrSDK({
network: NETWORK,
apiKey: "change-this-to-your-wif",
glittrApi: "https://devnet-core-api.glittr.fi", // devnet
electrumApi: "https://devnet-electrum.glittr.fi" // devnet
});
const creatorAccount = new Account({
wif: "change-this-to-your-wif",
network: NETWORK,
});
const sumArray = (arr: any[]) =>
arr.reduce((total, current) => total + current, 0);
async function mint() {
const contract: BlockTxTuple = [248437, 1];
// Get contract pair information from the AMM contract
const contractInfo = await client.getGlittrMessage(contract[0], contract[1]);
const firstContract: BlockTxTuple =
contractInfo.message.message.contract_creation.contract_type.mba
.mint_mechanism.collateralized.input_assets[0].glittr_asset;
const secondContract: BlockTxTuple =
contractInfo.message.message.contract_creation.contract_type.mba
.mint_mechanism.collateralized.input_assets[1].glittr_asset;
const address = creatorAccount.p2tr().address;
// Get your address' UTXO that contains the Assets
const inputAssetsFirst = await client.getAssetUtxos(
address,
firstContract[0] + ":" + firstContract[1]
);
const inputAssetsSecond = await client.getAssetUtxos(
address,
secondContract[0] + ":" + secondContract[1]
);
// Check if you don't have the assets
// Mint new assets if you don't have any
if (inputAssetsFirst.length == 0) {
throw new Error(
`You do not have assets for ${firstContract[0] + ":" + firstContract[1]}`
);
}
if (inputAssetsSecond.length == 0) {
throw new Error(
`You do not have assets for ${secondContract[0] + ":" + secondContract[1]}`
);
}
// Total of the assets
const totalHoldFirstAsset = sumArray(
inputAssetsFirst.map((item) => parseInt(item.assetAmount))
);
const totalHoldSecondAsset = sumArray(
inputAssetsSecond.map((item) => parseInt(item.assetAmount))
);
console.log(`Total hold ${firstContract} : ${totalHoldFirstAsset}`);
console.log(`Total hold ${secondContract} : ${totalHoldSecondAsset}`);
// Set how much you want to transfer for AMM liquidity
// And check if the amount is sufficient
const firstContractAmountForLiquidity = 100; // change this value
const secondContractAmountForLiquidity = 100; // change this value
if (firstContractAmountForLiquidity > totalHoldFirstAsset) {
throw new Error(`Amount for contract ${firstContract} insufficient`);
}
if (secondContractAmountForLiquidity > totalHoldSecondAsset) {
throw new Error(`Amount for contract ${secondContract} insufficient`);
}
const tx: OpReturnMessage = {
contract_call: {
contract,
call_type: {
mint: {
pointer: 1,
},
},
},
transfer: {
transfers: [
{
asset: firstContract,
output: 1,
amount: (
totalHoldFirstAsset - firstContractAmountForLiquidity
).toString(),
},
{
asset: secondContract,
output: 1,
amount: (
totalHoldSecondAsset - secondContractAmountForLiquidity
).toString(),
},
],
},
};
const utxos = await electrumFetchNonGlittrUtxos(client, address);
const nonFeeInputs: BitcoinUTXO[] = inputAssetsFirst
.concat(inputAssetsSecond)
.filter((value, index, arr) => {
const _value = JSON.stringify(value);
return index === arr.findIndex(obj => {
return JSON.stringify(obj) === _value;
});
}); // remove duplicates
const nonFeeOutputs: Output[] = [
{ script: txBuilder.compile(tx), value: 0 }, // Output #0 should always be OP_RETURN
{ address: address, value: 546 },
];
const { inputs, outputs } = await addFeeToTx(
NETWORK,
address,
utxos,
nonFeeInputs,
nonFeeOutputs
);
const txid = await client.createAndBroadcastRawTx({
account: creatorAccount.p2tr(),
inputs,
outputs,
});
console.log(`TXID : ${txid}`);
console.log("[+] Waiting to be mined");
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const message = await client.getGlittrMessageByTxId(txid);
console.log("Mined! Response", JSON.stringify(message));
break;
} catch (error) {
await new Promise((resolve) => setTimeout(resolve, 1));
}
}
}
mint();
This script will first check your address' UTXO for the two contract assets, checking if it is enough for the amount specified, and then mint the AMM LP token. Let's run the script.
yarn tsx 6-deposit-liquidity-amm.ts
Result:
$ yarn tsx 6-deposit-liquidity-amm.ts
yarn run v1.22.22
$ /Users/Projects/amm-example/node_modules/.bin/tsx 6-deposit-liquidity-amm.ts
Total hold 248419,1 : 100000
Total hold 248424,1 : 100000
TXID : 7f7748127acdac7077a6cbf4d20ac826e4f55506360a88839b877783c15ddfb4
[+] Waiting to be mined
Mined! Response {"block_tx":"248440:1","is_valid":true,"message":{"flaw":null,"message":{"contract_call":{"call_type":{"mint":{"pointer":1}},"contract":[248437,1]},"transfer":{"transfers":[{"amount":"99900","asset":[248419,1],"output":1},{"amount":"99900","asset":[248424,1],"output":1}]}}}}
✨ Done in 57.39s.
By default, the deposits are equal amounts (100 units each) of both tokens into the AMM pool. Let's take a look at your asset UTXOs after minting the liquidity pool token, you have now a new asset for the AMM liquidity pool token.
Finally, users can perform swaps between the two tokens, on this example we will use the 1st contract as input, and we will receive 2nd contract asset in return. Copy the file below into 7-swap-amm.ts. Change the apiKey, wifand also the two contract.
$ yarn tsx 7-swap-amm.ts
yarn run v1.22.22
$ /Users/Projects/amm-example/node_modules/.bin/tsx 7-swap-amm.ts
Total Input Asset: 99900
Input Amount: 10
[+] Calculated output amount is: 9
[+] Minimum output amount is: 8 (slippage: 10%)
TXID : 088ad6b2b41f18e84593465e2c8bec144cae230c257a8897399e274ca24efed5
[+] Waiting to be mined
Mined! Response {"block_tx":"248448:1","is_valid":true,"message":{"flaw":null,"message":{"contract_call":{"call_type":{"swap":{"assert_values":{"min_out_value":"8"},"pointer":1}},"contract":[248437,1]},"transfer":{"transfers":[{"amount":"99890","asset":[248419,1],"output":2}]}}}}
Asset output: {"assets":{"list":{"248424:1":99909,"248437:1":100}}}
✨ Done in 61.64s.
You have successfully swapped your asset here, let's take a look at the balance summary of your address. Your first asset will decrease by 10 units, while your second asset will increase by 9.
In this example, we use slippage tolerance to prevent transaction failures after the block is mined. The use of slippage in this way is still vulnerable to MEV, but better than without any assertion at all. We are open to any suggestions or PR to solve this problem.
Function explanations for setting the slippage tolerance:
calculateOutAmountis a function to calculate the result of a swap.
After obtaining the calculated output amount, decrease it with x% as slippage tolerance. So if the output amount changes (because of other swaps in the block), your tx will still be processed with a lower output amount.