MeshJS / mesh

An open-source library to advance Web3 development on Cardano
https://meshjs.dev
Apache License 2.0
188 stars 51 forks source link

Sending minted asset from AppWallet... but make user pay fee (via Multi Sig) #74

Closed pablosymc closed 1 year ago

pablosymc commented 1 year ago

Hi there, essentially what i'm looking to do... is send an already minted asset from the "AppWallet" to the user on the frontend via a multi sig transaction. The reason I'd like to do it via multi sig is because i'd like to have the user pay the transaction fee. Can someone assist me with guidance on how I could accomplish this?

I'm looking for a solution similar to https://github.com/MeshJS/minting-next-js-template/blob/main/pages/api/create-mining-transaction.js however instead of minting an asset to the user, i'd like to send an already minted asset from the "AppWallet" 😃

ltouro commented 1 year ago

That is tricky. You would need to know the user address, look at their UTXO set, craft a transaction that has your NFT and his ADA as inputs, set the outputs correctly (NFT, ADA and change) then partially sign the transaction. The user would need to receive the partially signed transaction (don't know if you can already to that onchain, otherwise, offchain mechanism required), verify the inputs/outputs (needs wallet support) , sign the [partial] transaction and send it back to the chain.

The alternative is to use a smart contract. For a minted token, you would lock the token in the contract address and make the contract allow the user withdraw given that a certain amount is being send to a certain [your] address. Then, you would need a dApp that knows where the token is locked and which conditions are necessary to unlock it to craft the transaction for the user. The user would sign the transaction built by the dApp with this wallet.

pablosymc commented 1 year ago

That is tricky. You would need to know the user address, look at their UTXO set, craft a transaction that has your NFT and his ADA as inputs, set the outputs correctly (NFT, ADA and change) then partially sign the transaction. The user would need to receive the partially signed transaction (don't know if you can already to that onchain, otherwise, offchain mechanism required), verify the inputs/outputs (needs wallet support) , sign the [partial] transaction and send it back to the chain.

The alternative is to use a smart contract. For a minted token, you would lock the token in the contract address and make the contract allow the user withdraw given that a certain amount is being send to a certain [your] address. Then, you would need a dApp that knows where the token is locked and which conditions are necessary to unlock it to craft the transaction for the user. The user would sign the transaction built by the dApp with this wallet.

Here's what I've got so far:

    const koiosProvider = new KoiosProvider("api");

    const appWallet = new AppWallet({
      networkId: 1,
      fetcher: koiosProvider,
      submitter: koiosProvider,
      key: {
        type: "mnemonic",
        words:[
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution",
          "solution"
        ],
      },
    });

    const appWalletAddress = appWallet.getPaymentAddress();

    // Get the user's UTXO's
    const user_utxos = await koiosProvider.fetchAddressUTxOs(recipientAddress);
    if (user_utxos.length === 0) {
      throw new Error("No user wallet UTXO's found");
    }

    // Get UTXO's that contains atleast 2 ADA + fees
    const selectedUserUtxos = largestFirst("2000000", user_utxos, true);
    if (selectedUserUtxos.length === 0) {
      throw new Error("No user wallet UTXO's available");
    }

    const assetMap = new Map();
    assetMap.set(
      "<asset>",
      amount.toString()
    );

    // Get the app wallet's UTXO's that contain the asset
    const wallet_utxos = await koiosProvider.fetchAddressUTxOs(
      appWalletAddress,
      "<asset>"
    );
    if (wallet_utxos.length === 0) {
      throw new Error("No wallet UTXO's found");
    }

     // Get UTXO that contains the asset
    const selectedWalletUtxos = largestFirstMultiAsset(
      assetMap,
      wallet_utxos,
      true
    );
    if (selectedWalletUtxos.length === 0) {
      throw new Error("No wallet UTXO's available");
    }

    const tx = new Transaction({ initiator: appWallet });

    // User supplies the UTXO's to cover fees
    tx.setTxInputs(selectedUserUtxos);

    // User sends 2 ADA to cover fee's
    tx.sendLovelace(appWalletAddress, "2000000");

    // User will receive change back to their wallet
    tx.setChangeAddress(recipientAddress);

    // App wallet supplies UTXO of asset to send
    tx.setTxInputs(selectedWalletUtxos);

    // App wallet sends asset to user
    tx.sendAssets(recipientAddress, [
      {
        unit: "<asset>",
        quantity: amount.toString(),
      },
    ]);

    // App wallet receives left over assets from UTXO
    tx.setChangeAddress(appWalletAddress);

    const unsignedTx = await tx.build();

Everything almost works as expected, except that the user supplying the fee UTXO (2+ ADA), isn't getting the correct change back. The "AppWallet" change seems to work fine however.

Any ideas what I could be doing wrong?

ltouro commented 1 year ago

Sorry, have no idea. I tried to take look at 'add_change_if_needed' fn in the utils module but could not find the implementation. Maybe some issue there?

pablosymc commented 1 year ago

More Info ... Here's the transaction output for the code I supplied above:

image

Inputs 1) User supplies the 5 ADA UTXO (to cover the required 2 ADA fee by us) (1st from top) 2) App Wallet supplies UTXO with the asset + Min ADA fee (2nd from top)

Outputs 1) User's 2 ADA fee is sent to the App Wallet (4th from top) 2) App wallet sends 100 Testing tokens to the user (5th from top) 3) App wallet receives change back from their supplied input (27.834537 ADA + User's supplied UTXO's ADA + The rest of the Testing asset from their original UTXO) (6th from top)

The issue here is that the user should receive change back from the 5 ADA supplied from their supplied UTXO (1st from top). They should receiving 5 ADA - 2 ADA (for the fee) - Tx fee.

Any ideas what I could be doing wrong... or how I can above to fixing this change issue?

jinglescode commented 1 year ago

its not complicated at all. i just did it. see tx: aff5a7523ad7c2e2f8172e84e601a12d2aa9dec321ec23697fb6c71a72f809ec

so the next step is to tweak the outputs. so you send back the change to the app wallet. for example in this case, you see the app wallet has 10 pieces, and lets say i only wanna send 1, so need to do, sendAssets and send 9 back. this just requires some javascript checking on the UTXOs.

Start template with Minting Next.js TypeScript

Backend:

import type { NextApiRequest, NextApiResponse } from "next";
import {
  AppWallet,
  Transaction,
  largestFirst,
  largestFirstMultiAsset,
  BlockfrostProvider,
} from "@meshsdk/core";
import { demoMnemonic } from "../../config/wallet";
import { costLovelace } from "../../config/mint";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const assetToSend =
    "d9312da562da182b02322fd8acb536f37eb9d29fba7c49dc172555274d657368546f6b656e";
  const assetToSendQty = "1";
  const recipientAddress = req.body.recipientAddress;
  const utxos = req.body.utxos;

  const blockchainProvider = new BlockfrostProvider("ADD_PREPROD_KEY_HERE");

  const appWallet = new AppWallet({
    networkId: 0,
    fetcher: blockchainProvider,
    submitter: blockchainProvider,
    key: {
      type: "mnemonic",
      words: demoMnemonic,
    },
  });

  const appWalletAddress = appWallet.getPaymentAddress();
  console.log("appWalletAddress", appWalletAddress);

  const app_wallet_utxos = await blockchainProvider.fetchAddressUTxOs(
    appWalletAddress,
    assetToSend
  );
  if (app_wallet_utxos.length === 0) {
    throw new Error("No wallet UTXO's found");
  }

  const assetMap = new Map();
  assetMap.set(assetToSend, assetToSendQty);
  const selectedWalletUtxos = largestFirstMultiAsset(
    assetMap,
    app_wallet_utxos,
    true
  );
  console.log("app wallet utxo", JSON.stringify(selectedWalletUtxos, null, 2));

  const selectedUtxos = largestFirst(costLovelace, utxos, true);
  console.log("user wallet utxos", JSON.stringify(selectedUtxos, null, 2));

  selectedUtxos.push.apply(selectedUtxos, selectedWalletUtxos);
  console.log("selectedUtxos", JSON.stringify(selectedUtxos, null, 2));

  const tx = new Transaction({ initiator: appWallet });
  tx.setTxInputs(selectedUtxos);

  tx.sendAssets(recipientAddress, [
    {
      unit: assetToSend,
      quantity: assetToSendQty,
    },
  ]);

  tx.sendLovelace(bankWalletAddress, costLovelace);

  // unless each UTXO in the app wallet is for one user, you might want to handle change to app wallet here
  // do javascript checking on app's UTXO. its easy because Mesh have already deserialized all the UTXO
  // tx.sendAssets(appWalletAddress, [
  //   {
  //     unit: assetToSend,
  //     quantity: assetToSendQty,
  //   },
  // ]);

  tx.setChangeAddress(recipientAddress);

  const appUnsignedTx = await tx.build();

  const unsignedTx = await appWallet.signTx(appUnsignedTx, true);

  res.status(200).json({ unsignedTx });
}

Frontend:

async function startMining() {
    setLoading(true);
    try {
      const recipientAddress = await wallet.getChangeAddress();
      const utxos = await wallet.getUtxos();

      const { unsignedTx } = await createTransaction(recipientAddress, utxos);

      const signedTx = await wallet.signTx(unsignedTx, true);

      const txHash = await wallet.submitTx(signedTx);

      setTxHash(txHash);
    } catch (error) {
      console.error(error);
    }
    setLoading(false);
  }

Signing:

Screenshot 2022-12-23 at 11 36 52 AM

tx: aff5a7523ad7c2e2f8172e84e601a12d2aa9dec321ec23697fb6c71a72f809ec