Wallchain
Search
K
🤲

SDK Integration

Integration with Wallchain SDK. The Wallchain SDK allows you to get an MEV rebate out of the box. No bindings to custom RPCs
Integration consists of 2 parts:
  1. 1.
    Upgrading the transaction through Wallchain SDK using an access key. Request one at https://t.me/wallchain_xyz
  2. 2.
    Sending the transaction to Wallchain Meta Swap Wrapper

Contents

Follow these steps to integrate the SDK into your platform:
Please refer to the Wallchain SDK GitHub repository for more details and examples.

1. Create SDK instance

import { SDK } '@wallchain/sdk';
const wallchain = new SDK({
provider //EIP-1193 provider
keys: {
matic: 'key',
...
/*
Api keys to Wallchain's contract.
Don't have keys? Contact our sales team.
*/
},
originator?: [] // platform addresses to recieve reward
originShare?: 0 //percent that you share with user, originShare ∈ [0, 50]
})

2. Check for MEV oppurtunity

const transactionData = {
from: 'address',
to: 'address',
data: 'bytes',
value: 'uint',
}
const apiResponse = await wallchain.checkMEV(transactionData);
In case MEV is found response will be shaped this way:
{
MEVFound: true,
cahsbackAmount: string, // in usd
masterInput: string
}

3. Allowance

Permit2 technology is being used to withdraw ERC-20 for executing the swap. No need to ask for permit if swapping native token of the chain.
const hasAllowance = await sdk.hasEnoughAllowance(sourceTokenAddress, ownerAddress, amount);
if (!hasAllowance) {
// returns Permit2 if chain supports, fallbacks to Wallchain contract
const spenderForAllowance = await sdk.getSpenderForAllowance();
// TODO: ask for allowance and continue
}

4. Permit

In case user swaps the ERC-20 token, we'd need to ask for a permit to execute the swap itself.
const spender = await sdk.getSpender(); // Wallchain's address at current chain
const witness = await sdk.signPermit(tokenAddress, wallet, spender, value);
This function automatically calls the wallet and resolves with signature and signed data.

5. Update transaction

Before sending a transaction to a wallet for confirmation, you need to upgrade it using Wallchain SDK at https://matic.wallchain.xyz/upgrade_txn/?key=<KEY>.
Here is an example of how it can be done:
const newTransaction = await sdk.createNewTransaction(
{
...originalTransaction,
isPermit, // true if not native token
amountIn, // amount of token to swap, equals msg.value when native token
srcTokenAddress,
dstTokenAddress
},
apiResp.masterInput // Resolved on step #1
witness, // resolved from step #4 or undefined
)
type newTransaction: {
from: string,
to: string,
data: string,
value: string,
gas: string
}
If the request fails for any reason you should use the original transaction. This way zero downtime is guaranteed.

Send Transaction to Wallchain Meta Swap Wrapper

Wallchain Meta Swap Wrapper redirects swaps to the original DEX router and triggers Wallchain Master to capture backrunning profits.
Wallchain contract example:
// SPDX-License-Identifier: UNLICENSED
/**
* Meta Swap Router Wrapper Contract.
* Designed by Wallchain in Metaverse.
*/
pragma solidity >=0.8.6;
import "./interfaces/IWChainMaster.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./interfaces/IAllowanceTransfer.sol";
import "./interfaces/ISignatureTransfer.sol";
contract MetaSwapWrapper is Ownable, ReentrancyGuard, Pausable {
using SafeERC20 for IERC20;
using EnumerableSet for EnumerableSet.AddressSet;
address private constant ETH_ADDRESS =
address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
event MasterUpgrade(address indexed old, address indexed newMaster);
event MasterError(string message);
event MasterError(bytes message);
event WithdrawAll();
event WithdrawEth();
event TargetAdded(address indexed target);
event TargetRemoved(address indexed target);
event NonProfitTransaction();
event MasterUpgradeFailed(address indexed attemptMaster);
struct WallchainExecutionParams {
address callTarget; // Target to call. Should create MEV opportunity.
address approveTarget; // Target to handle token transfers. Usually the same as callTarget.
bool isPermit; // Is approval handled through Permit.
bytes targetData; // Target transaction data.
bytes masterInput; // Generated strategies.
address[] originator; // Transaction beneficiaries.
uint256 amount; // Input token amount. Used in targetData for swap.
IERC20 srcToken; // Input token. Used in targetData for swap.
IERC20 dstToken; // Output token. Used in targetData for swap.
uint256 originShare; // Percentage share with the originator of the transaction.
ISignatureTransfer.PermitTransferFrom permit;
bytes signature;
}
IWChainMaster public wchainMaster;
EnumerableSet.AddressSet private whitelistedTargets;
ISignatureTransfer public immutable permit2;
constructor(
IWChainMaster _wchainMaster,
address[] memory _whitelistedTargets,
ISignatureTransfer _permit2
) {
wchainMaster = _wchainMaster;
for (uint256 i = 0; i < _whitelistedTargets.length; i++) {
whitelistedTargets.add(_whitelistedTargets[i]);
}
permit2 = _permit2;
}
receive() external payable {}
function whitelistedTargetsLength() external view returns (uint256) {
return whitelistedTargets.length();
}
function whitelistedTargetsAt(uint256 index)
external
view
returns (address)
{
return whitelistedTargets.at(index);
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function addTarget(address _target) external onlyOwner {
require(
!whitelistedTargets.contains(_target),
"Target is already added"
);
require(Address.isContract(_target), "Target must be a contract");
whitelistedTargets.add(_target);
emit TargetAdded(_target);
}
function removeTarget(address _target) external onlyOwner {
require(whitelistedTargets.contains(_target), "Target is not present");
whitelistedTargets.remove(_target);
emit TargetRemoved(_target);
}
function _isETH(IERC20 token) internal pure returns (bool) {
return (address(token) == ETH_ADDRESS);
}
function withdrawEth() external onlyOwner {
(bool result, ) = msg.sender.call{value: address(this).balance}("");
require(result, "Failed to withdraw Ether");
emit WithdrawEth();
}
function withdrawAll(address[] calldata tokens) external onlyOwner {
for (uint256 i = 0; i < tokens.length; i++) {
IERC20(tokens[i]).safeTransfer(
msg.sender,
_tokenBalance(tokens[i], address(this))
);
}
emit WithdrawAll();
}
function upgradeMaster() external onlyOwner {
address nextAddress = wchainMaster.nextAddress();
require(
Address.isContract(nextAddress),
"nextAddress must be a contract"
);
if (address(wchainMaster) != nextAddress) {
emit MasterUpgrade(address(wchainMaster), nextAddress);
wchainMaster = IWChainMaster(nextAddress);
return;
}
emit MasterUpgradeFailed(nextAddress);
}
function _tokenBalance(address token, address account)
internal
view
returns (uint256)
{
if (_isETH(IERC20(token))) {
return account.balance;
} else {
return IERC20(token).balanceOf(account);
}
}
function maybeApproveERC20(
IERC20 token,
uint256 amount,
address target,
address callTarget,
bool isPermit
) private {
// approve router to fetch the funds for swapping
if (isPermit) {
if (token.allowance(address(this), target) < amount) {
token.forceApprove(target, type(uint256).max);
}
IAllowanceTransfer(target).approve(
address(token),
callTarget,
uint160(amount),
uint48(block.timestamp)
);
} else {
if (token.allowance(address(this), target) < amount) {
token.forceApprove(target, amount);
}
}
}
function transferTokens(
address token,
address payable destination,
uint256 amount
) internal {
if (amount > 0) {
if (_isETH(IERC20(token))) {
(bool result, ) = destination.call{value: amount}("");
require(result, "Native Token Transfer Failed");
} else {
IERC20(token).safeTransfer(destination, amount);
}
}
}
function _processPermits(WallchainExecutionParams memory execution)
private
{
if (!_isETH(execution.srcToken)) {
maybeApproveERC20(
execution.srcToken,
execution.amount,
execution.approveTarget,
execution.callTarget,
execution.isPermit
);
permit2.permitTransferFrom(
execution.permit,
ISignatureTransfer.SignatureTransferDetails(
address(this),
execution.amount
),
msg.sender,
execution.signature
);
}
}
modifier coverUp(
bytes calldata masterInput,
address[] calldata originator,
uint256 originShare
) {
_;
// masterInput should be empty if txn is not profitable
if (masterInput.length > 8) {
try
wchainMaster.execute(
masterInput,
msg.sender,
originator,
originShare
)
{} catch Error(string memory _err) {
emit MasterError(_err);
} catch (bytes memory _err) {
emit MasterError(_err);
}
} else {
emit NonProfitTransaction();
}
}
function _validateInput(WallchainExecutionParams memory execution) private {
require(
whitelistedTargets.contains(execution.approveTarget) &&
whitelistedTargets.contains(execution.callTarget),
"Target must be whitelisted"
);
require(
execution.callTarget != address(permit2),
"Call target must not be Permit2"
);
if (_isETH(execution.srcToken)) {
require(
msg.value != 0,
"Value must be above 0 when input token is Native Token"
);
} else {
require(
msg.value == 0,
"Value must be 0 when input token is not Native Token"
);
}
{
bytes memory exchangeData = execution.targetData;
require(
exchangeData.length != 0,
"Transaction data must not be empty"
);
bytes32 selector;
assembly {
selector := mload(add(exchangeData, 32))
}
require(
bytes4(selector) != IERC20.transferFrom.selector,
"transferFrom not allowed for externalCall"
);
}
}
/// @return returnAmount The destination token sent to msg.sender
function swapWithWallchain(WallchainExecutionParams calldata execution)
external
payable
nonReentrant
whenNotPaused
coverUp(
execution.masterInput,
execution.originator,
execution.originShare
)
returns (uint256 returnAmount)
{
_validateInput(execution);
uint256 balanceBefore = _tokenBalance(
address(execution.dstToken),
address(this)
) - (_isETH(execution.dstToken) ? msg.value : 0);
uint256 srcBalanceBefore = _tokenBalance(
address(execution.srcToken),
address(this)
) - msg.value;
_processPermits(execution);
{
(bool success, ) = execution.callTarget.call{value: msg.value}(
execution.targetData
);
require(success, "Call Target failed");
}
uint256 balance = _tokenBalance(
address(execution.dstToken),
address(this)
);
uint256 srcBalance = _tokenBalance(
address(execution.srcToken),
address(this)
);
returnAmount = srcBalance - srcBalanceBefore;
if (srcBalance > srcBalanceBefore) {
transferTokens(
address(execution.srcToken),
payable(msg.sender),
returnAmount
);
}
returnAmount = balance - balanceBefore;
if (balance > balanceBefore) {
transferTokens(
address(execution.dstToken),
payable(msg.sender),
returnAmount
);
}
}
}

Gotchas

  1. 1.
    To get the information about expected profit and use it in the frontend, send the query to Wallchain Service API before the Swap button has been clicked.
  2. 2.
    No need to approve the router before the user clicks Swap, since the router might change
  3. 3.
    It's better to delay sending the request to Wallchain Service API from the last user keystroke by 200 ms