Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nft-swap): nft swap protocol v2 POC #2084

Merged
merged 84 commits into from
Apr 9, 2024
Merged

feat(nft-swap): nft swap protocol v2 POC #2084

merged 84 commits into from
Apr 9, 2024

Conversation

laruh
Copy link
Member

@laruh laruh commented Mar 10, 2024

Related to #900

To deploy smart contract you need to have it's abi json and bytes. They can be compiled from smart contract solidity code.

nft_swap_contract_abi.json and NFT_SWAP_CONTRACT_BYTES are compiled from EtomicSwapNft.sol contract from this PR

Solidity code of test ERC721 and ERC1155 tokens from which abi jsons and bytes were compiled:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract TestERC721 is ERC721 {
    constructor(string memory name, string memory symbol)
        ERC721(name, symbol)
    {}

    function mint(address to, uint256 tokenId) external {
        _mint(to, tokenId);
    }
}
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

contract TestERC1155 is ERC1155 {
    constructor(string memory uri) ERC1155(uri) {}

    function mint(
        address to,
        uint256 tokenId,
        uint256 amount,
        bytes memory data
    ) external {
        _mint(to, tokenId, amount, data);
    }
}

You can get abi json & bytes easily using https://remix.ethereum.org/ web IDE (PS dont forget to format code with shift+alt+F pls)
image

Some small note:
There was a moment that I wanted to validate event and topics of onERC1155Received and onERC721Received functions. But if payment was confirmed and its state is MakerPaymentSent in validation function, there is no need to check on...Received events.
Still would like to share a nice article about logs, topics and data objects in ETH receipt.
https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378

@laruh laruh added the in progress Changes will be made from the author label Mar 10, 2024
@laruh laruh changed the title feat(nft-swap): NFT Swap Protocol V2 POC feat(nft-swap): nft swap protocol v2 POC Mar 10, 2024
Copy link
Collaborator

@shamardy shamardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great start! We need to start paying more attention to the design choices early on before we commit to a suboptimal one.

mm2src/coins/eth.rs Outdated Show resolved Hide resolved
Comment on lines 6219 to 6222
self.rlp_append(&mut stream);
// there is minimal but risk that stream.out() may panic
debug_assert!(stream.is_finished(), "RlpStream must be finished before calling out");
Vec::from(stream.out())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use an empty vec if stream is not finished, this way we will avoid panicking and the preimage will be validated anyway and swap will fail if there is any problems in serialization.
P.S. Panicking here can be an attack vector if we are not careful since the preimage is received from the other side through a p2p message and can be made to crash the other side's node, right now it's won't be a problem probably since we use parse_preimage first but it can be a problem if we are not careful with code changes.

Copy link
Member Author

@laruh laruh Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use an empty vec if stream is not finished

Oh, exactly! yep we can do if stream.is_finished() check

right now it's won't be a problem probably since we use parse_preimage first but it can be a problem if we are not careful with code changes.

I see, we can refactor trait ToBytes impl and return Result<Vec, Error>. Well, it will trigger other code parts, so in some other traits we also have to do the refactoring and return Result type. For now I suggest to leave stream.is_finished() check,

impl ToBytes for SignedEthTx {
    fn to_bytes(&self) -> Vec<u8> {
        let mut stream = RlpStream::new();
        self.rlp_append(&mut stream);
        // Handle potential panicking.
        if stream.is_finished() {
            Vec::from(stream.out())
        } else {
            // TODO: Consider returning Result<Vec<u8>, Error> in future refactoring for better error handling.
            warn!("RlpStream was not finished; returning an empty Vec as a fail-safe.");
            vec![]
        }
    }
}

and in next pr(s) do the refactor and return Result where we will have to. Or at least try to refactor and see, do we actually have problems with it or not. I think its better to propagate errors instead of being careful (I mean we all are humans and there is a risk of miss something).

UPD: added stream.is_finished() check for now

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to propagate errors instead of being careful (I mean we all are humans and there is a risk of miss something).

I agree, I have the same problem with my_addr function in EVM HD wallet PR

async fn my_addr(&self) -> Self::Address {
match &self.as_ref().derivation_method {
DerivationMethod::SingleAddress(addr) => addr.clone(),
// Todo: Expect should not fail but we need to handle it properly
DerivationMethod::HDWallet(hd_wallet) => hd_wallet
.get_enabled_address()
.await
.expect("Getting enabled address should not fail!")
.address(),
}
}

To modify it I need to follow the same design choices @artemii235 did in the state machine swaps implementation, he would rather save a value to be retrieved later instead of calling the same function in multiple places and doing error handling in multiple places for the same function which makes the code harder to understand, and for such critical parts we need the code to be as clear as possible.

Comment on lines 1509 to 1530
/// Defines associated types specific to each coin (Pubkey, Address, etc.)
pub trait NftAssocTypes {
type TokenContractAddr: Send + Sync + fmt::Display;
type TokenContractAddrParseError: fmt::Debug + Send + fmt::Display;
type TokenId: ToBytes + Send + Sync;
type TokenIdParseError: fmt::Debug + Send + fmt::Display;
type Chain: ToBytes + Send + Sync;
type ChainParseError: fmt::Debug + Send + fmt::Display;
type ContractType: ToBytes + Send + Sync;
type ContractTypeParseError: fmt::Debug + Send + fmt::Display;

fn parse_token_contract_address(
&self,
token_contract_addr: &[u8],
) -> Result<Self::TokenContractAddr, Self::TokenContractAddrParseError>;

fn parse_token_id(&self, token_id: &[u8]) -> Result<Self::TokenId, Self::TokenIdParseError>;

fn parse_chain(&self, chain: &[u8]) -> Result<Self::Chain, Self::ChainParseError>;

fn parse_contract_type(&self, contract_type: &[u8]) -> Result<Self::ContractType, Self::ContractTypeParseError>;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we actually need those in swaps code? I am not sure yet, parsing data is used for p2p data and from storage data so maybe we will need for the from storage data but for p2p data I think the TokenContractAddr/TokenId/Chain/ContractType will be known for both sides before starting the swap. We can leave them for now but we have to be careful with adding coin specific logic inside swap state machine logic which we did in legacy swap and want to avoid in v2 code to make it cleaner.

Copy link
Member Author

@laruh laruh Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see two options of nft swap implementation:

  1. Have optional nft fields in current args structures, for example in SendMakerPaymentArgs. So in maker state changes we will need to check do we have Some in nft related fields or None. So in each maker state we will have to handle two inner implementations: for coin and for NFT. We may have the same problem as we had during trying to integrate nft activation into existing coin related code, which will lead to decision of logic separation.

  2. Create separate NFT related structures for args (eg SendNftMakerPaymentArgs) and NFT Maker states for MakerSwapStateMachine. This way we wont need to check optional nft fields in args as all of them will be required. Also we wont need to handle the coin and nft behaviors in current on_changed functions for coin maker states, as we will need to implement NFT related states. I believe we will still have same struct MakerSwapStateMachine.

I think the TokenContractAddr/TokenId/Chain/ContractType will be known for both sides before starting the swap

I wasnt sure about it so just decided to prepare the functionality. As we already use parse_... functions in maker_swap_v2.rs and taker_swap_v2.rs I decided that we may have nft related fields in args structures or negotiation data, so we need this.

The another big approach of nft implementation is providing in args structure(doesnt matter for now in NFT args or in optional nft field in current ags structures, its about having or not having NftAssocTypes) the string of concatenated "token_address,token_id".
This way Maker coin should be NFT global "coin" from the beginning I think, from its "nfts_infos" field we can get NftInfo value using "token_address,token_id" key. You know, this approach sounds not bad. This way we wont need NftAssocTypes. Need to refactor NFT activation and enable global NFT using nft json from coin config file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create separate NFT related structures for args (eg SendNftMakerPaymentArgs) and NFT Maker states for MakerSwapStateMachine

NFT states for swap state machines is a nice idea if it can be done cleanly :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure about it so just decided to prepare the functionality.

We can leave it in this PR and re-assess when implementing NFT functionalities in the swaps state machine.

@@ -1502,6 +1544,31 @@ pub struct SendMakerPaymentArgs<'a, Coin: CoinAssocTypes + ?Sized> {
pub swap_unique_data: &'a [u8],
}

pub struct SendNftMakerPaymentArgs<'a, Coin: CoinAssocTypes + NftAssocTypes + ?Sized> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, we shouldn't have maker payment args specific for NFTs. It will create a lot of conditional code in swaps code, you can leave it for now for the P.O.C. purpose but we need to have a plan on how we will integrate NFT swaps logic in current swap state machine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, its just one of two ways of two nft implementations from this #2084 (comment) description

Copy link
Member Author

@laruh laruh Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but we need to have a plan on how we will integrate NFT swaps logic in current swap state machine.

Oke, then we can think about approach with concatenated string of "token_address,token_id" parameter in Maker Payment args. If we want to reutulize the existing code, then we can add is_nft:bool param and optional nft_key:String.
If is_nft:true, nft_key must be Some, and we try to get NftInfo value from nfts_infos in global NFT. Well this means we will have to handle nft behavior and coin in same States.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If is_nft:true, nft_key must be Some, and we try to get NftInfo value from nfts_infos in global NFT. Well this means we will have to handle nft behavior and coin in same States.

Answered here #2084 (comment) , we will re-assess in next PRs I guess.

@@ -1575,6 +1642,38 @@ pub trait MakerCoinSwapOpsV2: CoinAssocTypes + Send + Sync + 'static {
async fn spend_maker_payment_v2(&self, args: SpendMakerPaymentArgs<'_, Self>) -> Result<Self::Tx, TransactionErr>;
}

#[async_trait]
pub trait MakerNftSwapOpsV2: CoinAssocTypes + NftAssocTypes + Send + Sync + 'static {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment, are you planning to make a different maker_swap_v2.rs and taker_swap_v2.rs files for NFTs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I wanted only to have NFT related states in maker_swap_v2.rs and taker_swap_v2.rs for MakerSwapStateMachine and TakerSwapStateMachine.
Well yeah for nft states we will need to require NFT related trait impl here impl<MakerCoin: MmCoin + MakerCoinSwapOpsV2, TakerCoin: MmCoin + TakerCoinSwapOpsV2> State

But we will discuss the nft swap approach of course.

Copy link
Collaborator

@shamardy shamardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next review iteration! Probably the last one :)

mm2src/coins/nft/trading_proto_v2/mod.rs Outdated Show resolved Hide resolved
mm2src/coins/nft/trading_proto_v2/mod.rs Outdated Show resolved Hide resolved
mm2src/coins/nft/trading_proto_v2/mod.rs Outdated Show resolved Hide resolved
mm2src/coins/lp_coins.rs Outdated Show resolved Hide resolved
mm2src/coins/nft/trading_proto_v2/mod.rs Outdated Show resolved Hide resolved
@laruh laruh removed the request for review from r2st April 5, 2024 15:38
shamardy
shamardy previously approved these changes Apr 8, 2024
Copy link
Collaborator

@shamardy shamardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

@shamardy
Copy link
Collaborator

shamardy commented Apr 8, 2024

@borngraced if you don't have anymore comments and all your review notes are resolved please approve this PR.

Copy link
Member

@borngraced borngraced left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!
here's my last review iteration

mm2src/coins/eth.rs Outdated Show resolved Hide resolved
mm2src/coins/eth.rs Show resolved Hide resolved
mm2src/coins/eth.rs Outdated Show resolved Hide resolved
mm2src/coins/eth.rs Show resolved Hide resolved
mm2src/coins/eth/nft_swap_v2/structs.rs Outdated Show resolved Hide resolved
Copy link
Member

@borngraced borngraced left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one more note please :)

Comment on lines 1466 to 1473
pub trait NftAssocTypes {
type ContractAddress: Send + Sync + fmt::Display;
type ContractAddrParseError: fmt::Debug + Send + fmt::Display;
type TokenId: ToBytes + Send + Sync;
type TokenIdParseError: fmt::Debug + Send + fmt::Display;
type ContractType: ToBytes + Send + Sync;
type ContractTypeParseError: fmt::Debug + Send + fmt::Display;

Copy link
Member

@borngraced borngraced Apr 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't we have just one error type that covers all error types? I mean a single, more informative error enum that encapsulates these different kinds of errors
e.g

 type Error: fmt::Debug + Send + fmt::Display;

Also, this trait name is better as NftParser or something

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't we have just one error type that covers all error types?

I followed the approach from CoinAssocTypes trait

impl<T: UtxoCommonOps> CoinAssocTypes for T {
type Address = Address;
type AddressParseError = MmError<AddrFromStrError>;
type Pubkey = Public;
type PubkeyParseError = MmError<keys::Error>;
type Tx = UtxoTx;
type TxParseError = MmError<serialization::Error>;
type Preimage = UtxoTx;
type PreimageParseError = MmError<serialization::Error>;
type Sig = Signature;
type SigParseError = MmError<secp256k1::Error>;

I suppose Artem decided to keep error types separately, as for UTXO he had errors from crates and usually our own enum errors already implement From for such types.

However, I agree that we can leave one error type for nft assoc trait, because usually we have to create our own structures/enum for this feature, so we cant avoid From trait implementation.

added one err here 95411dc also renamed eth error as its related to eth coin.

Also, this trait name is better as NftParser or something

I would like to disagree, CoinAssocTypes and NftAssocTypes traits are created to parse specific types associated with Coin or with NFT instances. we dont get the whole Coin or Nft.

Copy link
Member

@borngraced borngraced Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to disagree, CoinAssocTypes and NftAssocTypes traits are created to parse specific types associated with Coin or with NFT instances. we dont get the whole Coin or Nft.

doesn't mean it's a good name imo.. they can be rename to ParseCoinAssocTypes, ParseNftAssocTypes. currently, no one will ever guess what the trait is suppose to do just by looking at the name..I mean it doesn't tell any meaning

Copy link
Member

@borngraced borngraced left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀 Thank you very much for the great work!

@shamardy shamardy merged commit 6d45cfe into dev Apr 9, 2024
24 of 30 checks passed
@shamardy shamardy deleted the nft-swaps-poc branch April 9, 2024 14:06
dimxy added a commit to dimxy/komodo-defi-framework that referenced this pull request Apr 9, 2024
* dev:
  feat(nft-swap): nft swap protocol v2 POC (KomodoPlatform#2084)
  fix(zcoin): syncing and activation improvements (KomodoPlatform#2089)
  feat(crypto): mnemonic generation/encryption/decryption/storage (KomodoPlatform#2014)
  fix(eth): error handling in RPCs (KomodoPlatform#2090)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants