Skip to main content

5.3 Advanced

5.3.1 Multi-Call with Multi-Output Transactions

In this section, we explore complex integration scenarios involving multi-call and multi-output transactions. These advanced use-cases demonstrate the flexibility of the SurferMonkey SDK for managing multiple outputs with different smart contract calls and payloads.

Overview

Multi-call and multi-output transactions enable developers to manage multiple interactions within a single deposit, maintaining privacy while meeting complex requirements. This is useful for scenarios where a single input must be distributed across multiple outputs, each involving different smart contract calls, recipients, or assets.

Use Case: Complex Asset Distribution

Imagine a scenario where you need to distribute funds across several recipients while interacting with multiple smart contracts. For example:

  • Input: One input transaction locking a total of 10,000 units of an ERC20 token.
  • Output 1: Sends 4,000 units to multiple contracts for various purposes (e.g., staking, swapping).
  • Output 2: Transfers 6,000 units to multiple recipients, each receiving different amounts.

Configuration Steps

1. Define Input Transaction

Specify the asset and amount to be locked into the Universal Plugin:

info

ERC20 Deposits:

  • The field ASSET_ID is the ERC20 address.
const inputTx = {
AMOUNT_TOTAL: "10000", // Total amount in smallest units (e.g., wei)
ASSET_ID: "0xERC20TokenContractAddress" // ERC20 token contract address
};
2. Set Up Output Transactions

Define multiple outputs, each with its own smart contract calls:

const outputTxArr = [
{
amountOutput: "4000",
smartContractCalls: [
{
payloadAmountNative: "0",
targetSC: "0xContractAddressA",
payloadObject: {
functionHeader: "function stake(address paramA, uint256 paramB)",
functionName: "stake",
payloadParmsArr: ["0xRecipientAddressA", "1500"]
}
},
{
payloadAmountNative: "0",
targetSC: "0xContractAddressB",
payloadObject: {
functionHeader: "function swap(address paramA, uint256 paramB)",
functionName: "swap",
payloadParmsArr: ["0xRecipientAddressB", "2500"]
}
}
]
},
{
amountOutput: "6000",
smartContractCalls: [
{
payloadAmountNative: "0",
targetSC: "0xContractAddressC",
payloadObject: {
functionHeader: "function transfer(address paramA, uint256 paramB)",
functionName: "transfer",
payloadParmsArr: ["0xRecipientAddressC", "3800"]
}
},
{
payloadAmountNative: "0",
targetSC: "0xContractAddressD",
payloadObject: {
functionHeader: "function transfer(address paramA, uint256 paramB)",
functionName: "transfer",
payloadParmsArr: ["0xRecipientAddressD", "2200"]
}
}
]
}
];
3. Create User Message

Construct the userMessage object:

const userMessage = {
userEOA: "0xYourEvmAddressHere",
inputTx: inputTx,
outputTxArr: outputTxArr,
numberTxOut: 2,
pubKey: ["PubKeyString[0]", "PubKeyString[1]"],
chainID: "ChainIdentifier",
userMerkleProof: userMerkleProof
};
4. Generate Deposit and Lock Funds into the Universal Plugin

Call createDeposit() to create the deposit object and Verification Leaf ZKP.

info

ERC20 Deposits:

  • The msg.value must be set to 0.
  • Ensure the ERC20 token is approved for the Universal Plugin contract before depositing.
// Generate Deposit
const deposit = await SurferMonkey.createDeposit({ BACKEND_RPC, userMessage, verbose: true });

const UniversalPlugin_SC = new ethers.Contract("0xUniversalPluginAddress", UP_ABI, USER_SIGNER);
const submitDeposit = await UniversalPlugin_SC.Deposit(
deposit.SOLIDITY_DATA.a,
deposit.SOLIDITY_DATA.b,
deposit.SOLIDITY_DATA.c,
deposit.SOLIDITY_DATA.Input,
{
value: 0, // For ERC20, msg.value must be zero
maxPriorityFeePerGas: 25000000000,
maxFeePerGas: 27000000000,
gasLimit: 5142880
}
);
5. Generate Withdraw ZKP and Execute Output Transactions

Use createWithdraw() to generate the Zero-Knowledge Proof (ZKP) for each output transaction and withdraw.

for (let i = 0; i < userMessage.numberTxOut; i++) {
const getZKPSignalsObject = {
BACKEND_RPC,
depositJSON: deposit,
targetLeafChild: i,
verbose: true
};
const zkpSolidityData = await SurferMonkey.createWithdraw(getZKPSignalsObject);
console.log("Generated ZKP for output", i, ":", zkpSolidityData);

const TARGET_MIXER = new ethers.Contract("0xMixerAddress", MIXER_ABI, RELAYER_SIGNER);
const tx = await TARGET_MIXER.withdraw(
zkpSolidityData.a,
zkpSolidityData.b,
zkpSolidityData.c,
zkpSolidityData.Input,
deposit.depositArr[i].payloadData,
deposit.depositArr[i].targetSC,
deposit.depositArr[i].payloadAmountPerCall,
{
value: 0,
gasLimit: 2000000,
maxPriorityFeePerGas: 25000000000,
maxFeePerGas: 27000000000
}
);
console.log("Transaction submitted for output", i, ":", tx.hash);
}

Key Considerations

  • Asynchronous Withdrawals: Outputs can be withdrawn at different times, enabling staged releases of funds.
  • Atomic Multi-Calls: All smart contract calls within a single output occur atomically—ensuring that all actions succeed together, or none are executed.

Full Integration Code Snippet

Below is the complete code for this multi-call and multi-output example:

const inputTx = {
AMOUNT_TOTAL: "10000", // Total amount in smallest units (e.g., wei)
ASSET_ID: "0xERC20TokenContractAddress" // ERC20 token contract address
};

const outputTxArr = [
{
amountOutput: "4000",
smartContractCalls: [
{
payloadAmountNative: "0",
targetSC: "0xContractAddressA",
payloadObject: {
functionHeader: "function stake(address paramA, uint256 paramB)",
functionName: "stake",
payloadParmsArr: ["0xRecipientAddressA", "1500"]
}
},
{
payloadAmountNative: "0",
targetSC: "0xContractAddressB",
payloadObject: {
functionHeader: "function swap(address paramA, uint256 paramB)",
functionName: "swap",
payloadParmsArr: ["0xRecipientAddressB", "2500"]
}
}
]
},
{
amountOutput: "6000",
smartContractCalls: [
{
payloadAmountNative: "0",
targetSC: "0xContractAddressC",
payloadObject: {
functionHeader: "function transfer(address paramA, uint256 paramB)",
functionName: "transfer",
payloadParmsArr: ["0xRecipientAddressC", "3800"]
}
},
{
payloadAmountNative: "0",
targetSC: "0xContractAddressD",
payloadObject: {
functionHeader: "function transfer(address paramA, uint256 paramB)",
functionName: "transfer",
payloadParmsArr: ["0xRecipientAddressD", "2200"]
}
}
]
}
];

const userMerkleProof = await SurferMonkey.getInstitutionMembershipProof({
BACKEND_RPC,
userEOA: "0xYourEvmAddressHere",
institutionLeaves: ["0xValidUserAddress1", "0xValidUserAddress2", "0xYourEvmAddressHere"]
});

const userMessage = {
userEOA: "0xYourEvmAddressHere",
inputTx: inputTx,
outputTxArr: outputTxArr,
numberTxOut: 2,
pubKey: ["PubKeyString[0]", "PubKeyString[1]"],
chainID: "ChainIdentifier",
userMerkleProof: userMerkleProof
};

// Generate Deposit
const deposit = await SurferMonkey.createDeposit({ BACKEND_RPC, userMessage, verbose: true });

const UniversalPlugin_SC = new ethers.Contract("0xUniversalPluginAddress", UP_ABI, USER_SIGNER);
const submitDeposit = await UniversalPlugin_SC.Deposit(
deposit.SOLIDITY_DATA.a,
deposit.SOLIDITY_DATA.b,
deposit.SOLIDITY_DATA.c,
deposit.SOLIDITY_DATA.Input,
{
value: 0, // For ERC20, msg.value must be zero
maxPriorityFeePerGas: 25000000000,
maxFeePerGas: 27000000000,
gasLimit: 5142880
}
);

await submitDeposit.wait(); // Wait for transaction to get minted

// wait 10s for blockchain probabilistic finality
await new Promise(resolve => setTimeout(resolve, 10000));

// Generate Withdraw ZKP and execute output transactions
for (let i = 0; i < userMessage.numberTxOut; i++) {
const getZKPSignalsObject = {
BACKEND_RPC,
depositJSON: deposit,
targetLeafChild: i,
verbose: true
};
const zkpSolidityData = await SurferMonkey.createWithdraw(getZKPSignalsObject);
console.log("Generated ZKP for output", i, ":", zkpSolidityData);

const TARGET_MIXER = new ethers.Contract("0xMixerAddress", MIXER_ABI, RELAYER_SIGNER);
const tx = await TARGET_MIXER.withdraw(
zkpSolidityData.a,
zkpSolidityData.b,
zkpSolidityData.c,
zkpSolidityData.Input,
deposit.depositArr[i].payloadData,
deposit.depositArr[i].targetSC,
deposit.depositArr[i].payloadAmountPerCall,
{
value: 0,
gasLimit: 2000000,
maxPriorityFeePerGas: 25000000000,
maxFeePerGas: 27000000000
}
);
console.log("Transaction submitted for output", i, ":", tx.hash);
}