AMM Contract

Overview

This tutorial demonstrates how to create and use an AMM contract on Glittr:

  1. Create two assets

  2. Mint these assets

  3. Create an AMM contract

  4. Add liquidity to the AMM

  5. Perform swap with slippage tolerance

Prerequisites

  • Node.js and Yarn installed

  • Basic understanding of TypeScript

1. Project setup

Let's create a new directory on your machine:

mkdir amm-example
cd amm-example

Initialize the typescript project

yarn init

Just press Enter for every prompt:

$ yarn init
yarn init v1.22.22
warning ../package.json: No license field
warning ../../package.json: No license field
question name (amm-example):
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
✨  Done in 2.44s.

After that, let's add the dependencies

yarn add tsx typescript @glittr-sdk/sdk

2. Create a new Bitcoin Private Key

This tutorial will use a hardcoded bitcoin private key instead of using wallet. So let's first generate the private key using WIF:

You can create this online using https://learnmeabitcoin.com/technical/keys/private-key/wif/ (select testnet, and then copy the WIF Private Key ). \

After you obtain the WIF, create a new file address-check.tsin your project file, this will output the address. Replace the wifstring with yours.

address-check.ts
import { Account } from "@glittr-sdk/sdk";

const NETWORK = "regtest"

const account = new Account({
    wif: "cMqUkLHLtHJ4gSBdxAVtgFjMnHkUi5ZXkrsoBcpECGrE2tcJiNDh",
    network: NETWORK,
  });

console.log(account.p2tr().address);

Run the script with

yarn tsx address-check.ts
$ yarn tsx address-check.ts
yarn run v1.22.22
$ /amm-example/node_modules/.bin/tsx address-check.ts
bcrt1pvt3uzd3effh222v6wr8kx0sqhj07743lu0w9e7m74urqtgd85mgq6gwtes
✨  Done in 0.74s.

The address for my example is: bcrt1pvt3uzd3effh222v6wr8kx0sqhj07743lu0w9e7m74urqtgd85mgq6gwtes.

Next, let's get some BTC to that address.

  • Devnet: Faucet

  • Testnet4: mempool.space faucet

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.

1-create-1st-asset.ts
import { Account, GlittrSDK, OpReturnMessage } from "@glittr-sdk/sdk";

const NETWORK = "regtest";
const client = new GlittrSDK({
  network: NETWORK,
  apiKey: "change-this-to-your-api-key",
  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,
});

async function deployContract() {
  const tx: OpReturnMessage = {
    contract_creation: {
      contract_type: {
        moa: {
          ticker: "FIRST", // change this ticker
          divisibility: 18,
          live_time: 0,
          mint_mechanism: {
            free_mint: {
              amount_per_mint: BigInt(100000).toString(),
            },
          },
        },
      },
    },
  };

  const txid = await client.createAndBroadcastTx({
    account: creatorAccount.p2tr(),
    tx: tx,
  });

  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));
    }
  }
}

deployContract();

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.

const tx: OpReturnMessage = {
  contract_creation: {
    contract_type: {
      moa: {
        ticker: "SECOND",
        divisibility: 18,
        live_time: 0,
        mint_mechanism: {
          free_mint: {
            amount_per_mint: BigInt(100000).toString(),
          },
        },
      },
    },
  },
};

Run the script.

yarn tsx 2-create-2nd-asset.ts

Result:

$ 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.

3-mint-1st-asset.ts
import {
    Account,
    GlittrSDK,
    Output,
    OpReturnMessage,
    BlockTxTuple,
  } 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,
  });
  
  async function mint() {
    const contract: BlockTxTuple = [248419, 1]; // change this to your 1st contract BlockTx
  
    const tx: OpReturnMessage = {
      contract_call: {
        contract,
        call_type: {
          mint: {
            pointer: 1, 
          },
        },
      },
    };
  
    const address = creatorAccount.p2tr().address;
    const outputs: Output[] = [
      { address: address, value: 546 },
    ];
  
    const txid = await client.createAndBroadcastTx({
      account: creatorAccount.p2tr(),
      tx: tx,
      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();
  

Run the script.

yarn tsx 3-mint-1st-asset.ts

Result:

$ 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.

curl https://devnet-core-api.glittr.fi/helper/address/bcrt1pvt3uzd3effh222v6wr8kx0sqhj07743lu0w9e7m74urqtgd85mgq6gwtes/valid-outputs -H "Authorization: <your-api-key>"

{
  "block_height": 248432,
  "data": [
    {
      "address": "bcrt1pvt3uzd3effh222v6wr8kx0sqhj07743lu0w9e7m74urqtgd85mgq6gwtes",
      "asset_balances": [
        {
          "asset": null,
          "balance": "100000",
          "contract_id": "248419:1",
          "divisibility": 18,
          "is_state_key": null,
          "ticker": "FIRST",
          "type": {
            "free_mint": true
          }
        }
      ],
      "output": "19d61d826112da7d139fe0dd426d597482511de741d4285d313a6a87b1ad7bd4:1"
    },
    {
      "address": "bcrt1pvt3uzd3effh222v6wr8kx0sqhj07743lu0w9e7m74urqtgd85mgq6gwtes",
      "asset_balances": [
        {
          "asset": null,
          "balance": "100000",
          "contract_id": "248424:1",
          "divisibility": 18,
          "is_state_key": null,
          "ticker": "SECOND",
          "type": {
            "free_mint": true
          }
        }
      ],
      "output": "4bf339705da283cc5a98a32040852ac36e30e9267b84906a56a61a1fa4a1adf7:1"
    }
  ]
}

Step 4: Create the AMM Contract

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.

5-create-amm-asset.ts
import {
  Account,
  BlockTxTuple,
  GlittrSDK,
  OpReturnMessage,
  Output,
} 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,
});

async function deployContract() {
  const firstContract: BlockTxTuple = [248419, 1]; // change this to your first contract's
  const secondContract: BlockTxTuple = [248424, 1]; // change this to your second contract's
  const tx: OpReturnMessage = {
    contract_creation: {
      contract_type: {
        mba: {
          ticker: "AMM", // change this to something more unique
          divisibility: 18,
          live_time: 0,
          mint_mechanism: {
            collateralized: {
              input_assets: [
                {
                  glittr_asset: firstContract,
                },
                {
                  glittr_asset: secondContract,
                },
              ],
              _mutable_assets: false,
              mint_structure: {
                proportional: {
                  ratio_model: "constant_product",
                  // inital_mint_pointer_to_key: 1
                },
              },
            },
          },
          burn_mechanism: {},
          swap_mechanism: {},
        },
      },
    },
  };

  const address = creatorAccount.p2tr().address;
  const outputs: Output[] = [
    { address: address, value: 546 },
  ];

  const txid = await client.createAndBroadcastTx({
    account: creatorAccount.p2tr(),
    outputs: outputs,
    tx: tx,
  });

  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));
    }
  }
}

deployContract();

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.

curl https://devnet-core-api.glittr.fi/helper/address/bcrt1pvt3uzd3effh222v6wr8kx0sqhj07743lu0w9e7m74urqtgd85mgq6gwtes/valid-outputs -H "Authorization: your-api-key"

...
        {
          "asset": null,
          "balance": "100",
          "contract_id": "248437:1",
          "divisibility": 18,
          "is_state_key": null,
          "ticker": "AMM",
          "type": {
            "collateralized": {
              "assets": [
                {
                  "contract_id": "248419:1",
                  "divisibility": 18,
                  "ticker": "FIRST"
                },
                {
                  "contract_id": "248424:1",
                  "divisibility": 18,
                  "ticker": "SECOND"
                }
              ]
            }
          }
        },
...

Step 6: Perform Swap

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.

7-swap-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);

const contractTupleToString = (contract: [number, number]) => {
  return contract[0] + ":" + contract[1];
};

const calculateOutAmount = async (
  contract: [number, number],
  contractInput: [number, number],
  amount: number
): Promise<number> => {
  const contractState = await client.getContractState(contract[0], contract[1]);

  const outputContract = Object.keys(
    contractState.collateralized.amounts
  ).filter((item: string) => item !== contractTupleToString(contractInput))[0]!;

  const inputTotalSupply =
    contractState.collateralized.amounts[contractTupleToString(contractInput)];
  const outputTotalSupply =
    contractState.collateralized.amounts[outputContract];

  const outputAmount = Math.floor(
    outputTotalSupply -
      (inputTotalSupply * outputTotalSupply) / (inputTotalSupply + amount)
  );

  if (outputAmount == 0) {
    throw new Error("Calculated output amount is 0");
  }

  return outputAmount;
};

async function mint() {
  // Swap the first asset with the second asset
  const contract: BlockTxTuple = [27082, 1];
  const firstContract: BlockTxTuple = [27048, 1];
  const address = creatorAccount.p2tr().address;

  const inputAssetsFirst = await client.getAssetUtxos(
    address,
    firstContract[0] + ":" + firstContract[1]
  );

  const totalInput = sumArray(
    inputAssetsFirst.map((item) => parseInt(item.assetAmount))
  );
  const totalInputUsed = 10;
  console.log(`Total Input Asset: ${totalInput}`);
  console.log(`Input Amount: ${totalInputUsed}`);

  // Slippage calculation
  const outAmount = await calculateOutAmount(
    contract,
    firstContract,
    totalInputUsed
  );
  console.log(`[+] Calculated output amount is: ${outAmount}`);
  const slippagePercentage = 10;
  const minOutputAmount = Math.floor(outAmount - (outAmount * 10) / 100);
  console.log(
    `[+] Minimum output amount is: ${minOutputAmount} (slippage: ${slippagePercentage}%)`
  );

  const tx: OpReturnMessage = {
    contract_call: {
      contract,
      call_type: {
        swap: {
          pointer: 1,
          assert_values: { min_out_value: minOutputAmount.toString() },
        },
      },
    },
    transfer: {
      transfers: [
        {
          asset: firstContract, 
          output: 2,
          amount: (totalInput - totalInputUsed).toString(), // just use 10 for swap, the reset will be change for the account
        },
      ],
    },
  };

  const utxos = await electrumFetchNonGlittrUtxos(client, address);

  const nonFeeInputs: BitcoinUTXO[] = inputAssetsFirst;
  const nonFeeOutputs: Output[] = [
    { script: txBuilder.compile(tx), value: 0 }, // Output #0 should alwasy be OP_RETURN
    { address: address, value: 546 },
    { 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));
      const assetOutput = await client.getGlittrAsset(txid, 1);
      console.log("Asset output: ", assetOutput);
      break;
    } catch (error) {
      await new Promise((resolve) => setTimeout(resolve, 1));
    }
  }

}

mint();

Let's run the script to perform the swap.

yarn tsx 7-swap-amm.ts

Result:

$ 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.

$ curl https://devnet-core-api.glittr.fi/helper/address/bcrt1pvt3uzd3effh222v6wr8kx0sqhj07743lu0w9e7m74urqtgd85mgq6gwtes/balance-summary -H "Authorization: your-api-key"

{
  "block_height": 248450,
  "data": [
    {
      "asset": null,
      "balance": "100",
      "contract_id": "248437:1",
      "divisibility": 18,
      "is_state_key": null,
      "ticker": "AMM",
      "type": {
        "collateralized": {
          "assets": [
            {
              "contract_id": "248419:1",
              "divisibility": 18,
              "ticker": "FIRST"
            },
            {
              "contract_id": "248424:1",
              "divisibility": 18,
              "ticker": "SECOND"
            }
          ]
        }
      }
    },
    {
      "asset": null,
      "balance": "99890",
      "contract_id": "248419:1",
      "divisibility": 18,
      "is_state_key": null,
      "ticker": "FIRST",
      "type": {
        "free_mint": true
      }
    },
    {
      "asset": null,
      "balance": "99909",
      "contract_id": "248424:1",
      "divisibility": 18,
      "is_state_key": null,
      "ticker": "SECOND",
      "type": {
        "free_mint": true
      }
    }
  ]
}

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.

const calculateOutAmount = async (
  contract: [number, number],
  contractInput: [number, number],
  amount: number
): Promise<number> => {
  const contractState = await client.getContractState(contract[0], contract[1]);

  const outputContract = Object.keys(
    contractState.collateralized.amounts
  ).filter((item: string) => item !== contractTupleToString(contractInput))[0]!;

  const inputTotalSupply =
    contractState.collateralized.amounts[contractTupleToString(contractInput)];
  const outputTotalSupply =
    contractState.collateralized.amounts[outputContract];

  const outputAmount = Math.floor(
    outputTotalSupply -
      (inputTotalSupply * outputTotalSupply) / (inputTotalSupply + amount)
  );

  if (outputAmount == 0) {
    throw new Error("Calculated output amount is 0");
  }

  return outputAmount;
};

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.

  // Slippage calculation
  const outAmount = await calculateOutAmount(
    contract,
    firstContract,
    totalInputUsed
  );
  console.log(`[+] Calculated output amount is: ${outAmount}`);
  const slippagePercentage = 10;
  const minOutputAmount = Math.floor(outAmount - (outAmount * 10) / 100);
  console.log(
    `[+] Minimum output amount is: ${minOutputAmount} (slippage: ${slippagePercentage}%)`
  );

Last updated