A guide on how to set up cross-chain license minting using deBridge.
In this tutorial, we will explore how to use deBridge to perform cross-chain license minting. For this tutorial specifically, we’ll be using Base $ETH to mint a license on Story.
The first step is to construct a deBridge API call. The purpose of this API call is to receive back a response that will contain transaction data so we can then execute it on the source chain.This deBridge order swaps tokens from one chain to another. We can also optionally attach a dlnHook that will execute an arbitrary action upon order completion (ex. after $ETH has been swapped for $IP). This is where the magic happens.
In this case, the dlnHook will be a call to mintLicenseTokensCrossChain, which is a function in this contract that wraps the received $IP to $WIP and then mints a license token on Story.You can see that mintLicenseTokensCrossChain looks like this:
DebridgeLicenseTokenMinter.sol
Copy
Ask AI
function mintLicenseTokensCrossChain( address licensorIpId, uint256 licenseTermsId, uint256 tokenAmount, address receiver) external payable { uint256 amount = msg.value; require(amount > 0, "DebridgeLicenseTokenMinter: zero amount"); // Create multicall data - using memory array directly to avoid stack issues IMulticall.Call3Value[] memory calls = new IMulticall.Call3Value[](2); // First call: deposit IP to get WIP tokens calls[0] = IMulticall.Call3Value({ target: WIP, allowFailure: false, value: amount, callData: abi.encodeWithSelector(IWrappedIP.deposit.selector) }); // Second call: mint license tokens calls[1] = IMulticall.Call3Value({ target: LICENSING_MODULE, allowFailure: false, value: 0, callData: abi.encodeWithSelector( ILicensingModule.mintLicenseTokens.selector, licensorIpId, PIL_TEMPLATE, licenseTermsId, tokenAmount, receiver, "", 0, 100_000_000 ) }); // Execute multicall and emit event IMulticall.Result[] memory returnData = IMulticall(MULTICALL).aggregate3Value{ value: amount }(calls); bytes memory raw = returnData[1].returnData; uint256 startLicenseTokenId = abi.decode(raw, (uint256)); // if mintLicenseTokens returns uint256 emit LicenseTokensMinted(licensorIpId, receiver, startLicenseTokenId, tokenAmount);}
You may be wondering, “where does it approve the Royalty Module (what pays for the license minting fee) to spend the $WIP?” This is already done for you, since the Royalty Module is already approved to spend on behalf of Multicall.The reason we had to make a Multicall contract in the first place, instead of simply bridging directly to $WIP and calling mintLicenseTokens in the Licensing Module, is because the Royalty Module wouldn’t be approved to spend the $WIP. So we utilize the Multicall contract to take care of that for us instead.
To summarize, we will construct a deBridge API call that says “we want to swap $ETH for $IP, then use a dlnHook to call a smart contract on Story that wraps $IP to $WIP and mints a license on Story”.
Now that we have the dlnHook, we can construct the whole deBridge API call, including the dlnHook.
You can view deBridge’s documentation on the create-tx endpoint here. I also highly recommend checking out the Swagger UI for the create-tx endpoint as well.
Attribute
Description
srcChainId
The ID of the source blockchain (e.g., Ethereum mainnet is 1).
srcChainTokenIn
The address of the token being swapped on the source chain (ETH in this case).
srcChainTokenInAmount
The amount of the source token to swap, set to auto for automatic calculation.
dstChainId
The ID of the destination blockchain (e.g., Story mainnet is 100000013).
dstChainTokenOut
The address of the token to receive on the destination chain (WIP token).
dstChainTokenOutAmount
The amount of the destination token to receive. It should be the same as the amount we’re paying in payRoyaltyOnBehalf in step 1a.
dstChainTokenOutRecipient
This can just be the same as senderAddress.
senderAddress
The address initiating the transaction.
srcChainOrderAuthorityAddress
The address authorized to manage the order on the source chain. This can just be the same as senderAddress.
dstChainOrderAuthorityAddress
The address authorized to manage the order on the destination chain. This can just be the same as senderAddress.
enableEstimate
A flag to enable transaction simulation and estimation.
prependOperatingExpenses
A flag to include operating expenses in the transaction.
dlnHook
The URL-encoded hook that specifies additional actions to execute post-swap.
main.ts
Copy
Ask AI
import { base } from "viem/chains";// ... previous code here ...// Build deBridge API URL for cross-chain royalty paymentconst buildDeBridgeApiUrl = ({ ipId: `0x${string}`, licenseTermsId: bigint, receiverAddress: `0x${string}`, senderAddress: `0x${string}`, paymentAmount: string // should be in wei}): string => { const dlnHook = buildRoyaltyPaymentHook({ ipId, licenseTermsId, receiverAddress }); const encodedHook = encodeURIComponent(dlnHook); const url = `https://dln.debridge.finance/v1.0/dln/order/create-tx?` + `srcChainId=${base.id}` + // we make this zero address which represents the native token ($ETH) `&srcChainTokenIn=0x0000000000000000000000000000000000000000` + // we set this to auto which will automatically calculate the amount of $ETH to swap `&srcChainTokenInAmount=auto` + // Story's mainnet chain ID `&dstChainId=100000013` + // we make this zero address which represents the native token ($IP) `&dstChainTokenOut=0x0000000000000000000000000000000000000000` + // we set the amount of $IP to pay for the license on Story `&dstChainTokenOutAmount=${paymentAmount}` + // the address of the contract that will wrap the $IP and mint the license on Story `&dstChainTokenOutRecipient=${senderAddress}` + // the address of the user initiating the transaction `&senderAddress=${senderAddress}` + // the address authorized to manage the order on the source chain `&srcChainOrderAuthorityAddress=${senderAddress}` + // the address authorized to manage the order on the destination chain `&dstChainOrderAuthorityAddress=${senderAddress}` + // we set this to true to enable transaction simulation and estimation `&enableEstimate=true` + // we set this to true to include operating expenses in the transaction `&prependOperatingExpenses=true` + `&dlnHook=${encodedHook}`; return url;};
Once the API call is constructed, execute it to receive a response. This response includes transaction data and an estimate for running the transaction on the source swap chain (e.g., Ethereum, Solana).
Copy
Ask AI
// ... previous code here ...const getDeBridgeTransactionData = async ({ ipId: `0x${string}`, licenseTermsId: bigint, receiverAddress: `0x${string}`, senderAddress: `0x${string}`, paymentAmount: string // should be in wei}): Promise<DeBridgeApiResponse> => { try { const apiUrl = buildDeBridgeApiUrl({ ipId, licenseTermsId, receiverAddress, senderAddress, paymentAmount }); const response = await fetch(apiUrl, { method: "GET", headers: { Accept: "application/json", }, }); if (!response.ok) { throw new Error( `deBridge API error: ${response.status} ${response.statusText}` ); } const data = (await response.json()) as DeBridgeApiResponse; // Validate the response if (!data.tx || !data.estimation || !data.orderId) { throw new Error("Invalid deBridge API response: missing required fields"); } return data; } catch (error) { console.error("Error calling deBridge API:", error); throw error; }};
Step 3: Executing the Transaction on the Source Chain
Next, you would take the API response and execute the transaction on the source chain.
View the docs here on submitting the transaction, including how this would be done differently on Solana.
TypeScript
Copy
Ask AI
import { base } from "viem/chains";import { createWalletClient, http, WalletClient, parseEther } from "viem";import { privateKeyToAccount, Address, Account } from "viem/accounts";import dotenv from "dotenv";dotenv.config();// Validate environment variablesif (!process.env.WALLET_PRIVATE_KEY) { throw new Error("WALLET_PRIVATE_KEY is required in .env file");}// Create account from private keyconst account: Account = privateKeyToAccount( `0x${process.env.WALLET_PRIVATE_KEY}` as Address);// Initialize the wallet clientconst walletClient = createWalletClient({ chain: base, transport: http("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"), // Use Infura or another Ethereum provider account,}) as WalletClient;// ... previous code here ...// Function to send a transactionconst executeLicenseMint = async (params: { ipId: `0x${string}`; licenseTermsId: bigint; receiverAddress: `0x${string}`; senderAddress: `0x${string}`; paymentAmount: string; // should be in wei}) => { // Get transaction data from deBridge const deBridgeResponse = await getDeBridgeTransactionData(params); try { // Execute the transaction using the user's wallet const txHash = await walletClient.sendTransaction({ to: deBridgeResponse.tx.to as Address, data: deBridgeResponse.tx.data as Address, value: BigInt(deBridgeResponse.tx.value), account: account as Account, chain: base, }); console.log("Transaction sent:", txHash); // Wait for the transaction to be mined const receipt = await walletClient.waitForTransactionReceipt(txHash); console.log("Transaction mined:", receipt.transactionHash); } catch (error) { console.error("Error sending transaction:", error); }};// Example usage with a mock API responseconst params = { ipId: "0xcb6B9CCae4108A103097B30cFc25e1E257D4b5Fe", licenseTermsId: BigInt(27910), receiverAddress: "0x01", // replace with the address that should receive the license on Story senderAddress: account.address, paymentAmount: parseEther("0.00001"), // 0.00001 $WIP since the license on Story costs 0.00001 $WIP};// Execute the function to send the transactionexecuteLicenseMint(params);