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