In this tutorial, we will explore how to use deBridge to perform cross-chain royalty payments and arbitrary transactions. For this tutorial specifically, we’ll be using Base $ETH to mint a license on Story. From a high level, it involves:
  1. Constructing a deBridge API call that will return tx data to swap tokens across chains and perform some action (e.g. mint a license on Story)
  2. Executing the API call to receive that tx data
  3. Executing the transaction (using the returned tx data) on the source chain
Cross-Chain Royalty Payments

Step 1: Constructing the deBridge API Call

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 $WIP). This is where the magic happens.
You can learn more about dlnHooks here.
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.
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”.

Step 1a. Constructing the dlnHook

The dlnHook is a JSON object that will be attached to the deBridge API call. It will contain the following information:
  • The type of action to execute (evm_transaction_call)
  • The address of the contract to call (ROYALTY_MODULE)
  • The calldata to execute (mintLicenseTokensCrossChain)
main.ts
const STORY_WRAP_THEN_LICENSE_MULTICALL =
  "0x6429a616f76a8958e918145d64bf7681c3936d6a";

// Build the dlnHook for Story royalty payment
const buildRoyaltyPaymentHook = ({ ipId: `0x${string}`, licenseTermsId: bigint, receiverAddress: `0x${string}` }): string => {
  // Encode the mintLicenseTokensCrossChain function call
  const calldata = encodeFunctionData({
    abi: [
      {
        name: "mintLicenseTokensCrossChain",
        type: "function",
        inputs: [
          { name: "licensorIpId", type: "address" },
          { name: "licenseTermsId", type: "uint256" },
          { name: "tokenAmount", type: "uint256" },
          { name: "receiver", type: "address" },
        ],
      },
    ],
    functionName: "mintLicenseTokensCrossChain",
    args: [
      ipId,
      licenseTermsId,
      BigInt(1),
      receiverAddress,
    ],
  });

  // Build the dlnHook JSON
  const dlnHook = {
    type: "evm_transaction_call",
    data: {
      to: STORY_WRAP_THEN_LICENSE_MULTICALL,
      calldata: calldata,
      gas: 0,
    },
  };

  return JSON.stringify(dlnHook);
};

Step 1b. Constructing the deBridge API Call

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.
AttributeDescription
srcChainIdThe ID of the source blockchain (e.g., Ethereum mainnet is 1).
srcChainTokenInThe address of the token being swapped on the source chain (ETH in this case).
srcChainTokenInAmountThe amount of the source token to swap, set to auto for automatic calculation.
dstChainIdThe ID of the destination blockchain (e.g., Story mainnet is 100000013).
dstChainTokenOutThe address of the token to receive on the destination chain (WIP token).
dstChainTokenOutAmountThe amount of the destination token to receive. It should be the same as the amount we’re paying in payRoyaltyOnBehalf in step 1a.
dstChainTokenOutRecipientThis can just be the same as senderAddress.
senderAddressThe address initiating the transaction.
srcChainOrderAuthorityAddressThe address authorized to manage the order on the source chain. This can just be the same as senderAddress.
dstChainOrderAuthorityAddressThe address authorized to manage the order on the destination chain. This can just be the same as senderAddress.
enableEstimateA flag to enable transaction simulation and estimation.
prependOperatingExpensesA flag to include operating expenses in the transaction.
dlnHookThe URL-encoded hook that specifies additional actions to execute post-swap.
main.ts
import { base } from "viem/chains";

// ... previous code here ...

// Build deBridge API URL for cross-chain royalty payment
const 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=${STORY_WRAP_THEN_LICENSE_MULTICALL}` +
    // 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;
};

Step 2: Executing the API Call

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).
// ... 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
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 variables
if (!process.env.WALLET_PRIVATE_KEY) {
  throw new Error("WALLET_PRIVATE_KEY is required in .env file");
}

// Create account from private key
const account: Account = privateKeyToAccount(
  `0x${process.env.WALLET_PRIVATE_KEY}` as Address
);

// Initialize the wallet client
const 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 transaction
const 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 response
const 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 transaction
executeLicenseMint(params);

Conclusion

Congratulations! You have successfully set up cross-chain royalty payments using deBridge.