Sign and Broadcast Transaction

To complete any action using the DeFi API, sign the final unsigned transaction and broadcast it to the network.

1. Prepare unsigned transaction

Obtain the unsigned transaction object either in the encoded or json format provided by the Craft Transaction Step endpoint.

2. Sign transaction

Since the DeFi API flow is determined by the individual schema depending on the action type, protocol, and strategy, construction of multiple transactions may be required. As a result, there are two types of unsigned transaction objects provided by the DeFi API — intermediate and final.

  • Intermediate transactions are typically required to grant permissions to smart contracts involved in the integration process. These objects need a signature, as they must be included in the payload of the final transaction, but there is no need to broadcast such transactions to the network.

  • Final transactions, on the other hand, require both signing and broadcasting. The steps at which these transactions are constructed have the word "final" in their name and are executed last according to the schema's order. Such objects must contain the data of all intermediate transactions in the payload.

Check your transaction object type and sign it using the examples of code provided below.

🚧

Note that for intermediate and final types, different signing libraries are used.

It is possible to sign the final transaction with the code for the intermediate one, but this will lead to the validation error on attempting to broadcast such a transaction to the network.

For intermediate transactions

NB: signing only, no broadcasting.

Use any of the code examples for the base network below.

import {
  signTypedData,
  sign,
  serializeSignature,
  privateKeyToAccount,
} from 'viem/accounts';
import { createWalletClient, custom } from 'viem';
import { base } from 'viem/chains';

// Example API response shape
// response = { json: { domain, types, primaryType, message }, encoded: "0x..." }

// -----------------------------------------------------------------------------
// 1. With private key — use response.json (typedData)
// -----------------------------------------------------------------------------
async function signResponseWithPrivateKey(privateKey, response) {
  const privateKeyHex = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`;
  const account = privateKeyToAccount(privateKeyHex);
  const { json: typedData } = response;

  return signTypedData({
    account,
    domain: typedData.domain,
    types: typedData.types,
    primaryType: typedData.primaryType,
    message: typedData.message,
  });
}


// -----------------------------------------------------------------------------
// 2. With private key — use response.encoded (hash only)
// -----------------------------------------------------------------------------
async function signResponseHashWithPrivateKey(privateKey, response) {
  const privateKeyHex = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`;
  const hashHex = response.encoded.startsWith('0x') ? response.encoded : `0x${response.encoded}`;

  const sig = await sign({
    privateKey: privateKeyHex,
    hash: hashHex,
  });
  return serializeSignature(sig);
}
import {
  signTypedData,
  sign,
  serializeSignature,
  privateKeyToAccount,
} from 'viem/accounts';
import { createWalletClient, custom } from 'viem';
import { base } from 'viem/chains';

// Example API response shape
// response = { json: { domain, types, primaryType, message }, encoded: "0x..." }

// -----------------------------------------------------------------------------
// 3. Via provider (frontend) — use response.json (typedData), preferred
// -----------------------------------------------------------------------------
async function signResponseViaProvider(provider, response) {
  const walletClient = createWalletClient({
    chain: base,
    transport: custom(provider),
  });

  const [address] = await walletClient.getAddresses();
  if (!address) throw new Error('Wallet not connected');

  const { json: typedData } = response;
  return walletClient.signTypedData({
    account: address,
    domain: typedData.domain,
    types: typedData.types,
    primaryType: typedData.primaryType,
    message: typedData.message,
  });
}

// -----------------------------------------------------------------------------
// 4. Via provider (frontend) — use response.encoded (hash only), eth_sign
// -----------------------------------------------------------------------------
async function signResponseHashViaProvider(provider, response, address) {
  const hashHex = response.encoded.startsWith('0x') ? response.encoded : `0x${response.encoded}`;
  if (hashHex.length !== 66) throw new Error('Hash must be 32 bytes (66 hex chars with 0x)');

  return provider.request({
    method: 'eth_sign',
    params: [address, hashHex],
  });
}

For final transactions

NB: contain the word "final" in the step name; require signing and broadcasting.

Use any of the code examples for the base network below.

import { createWalletClient, custom, http, parseTransaction } from 'viem';
import { privateKeyToAccount, signTransaction } from 'viem/accounts';
import { base } from 'viem/chains';


// Example API response shape
// response = { json: { domain, types, primaryType, message }, encoded: "0x..." }

// -----------------------------------------------------------------------------
// 1. With private key — use response.encoded (raw hex), then sign
// -----------------------------------------------------------------------------
async function signRawTransactionWithPrivateKey(privateKey, response, rpcUrl) {
  const privateKeyHex = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`;
  const account = privateKeyToAccount(privateKeyHex);
  const rawTxHex = response.encoded.startsWith('0x') ? response.encoded : `0x${response.encoded}`;

  const transaction = parseTransaction(rawTxHex);
  const walletClient = createWalletClient({
    chain: base,
    account,
    transport: http(rpcUrl),
  });

  const request = await walletClient.prepareTransactionRequest({
    ...transaction,
    gas: transaction.gas,
  });

  return walletClient.signTransaction(request); // signed hex "0x02..."
}

// -----------------------------------------------------------------------------
// 2. With private key — use response.json (transaction object), then sign
// -----------------------------------------------------------------------------
async function signTransactionObjectWithPrivateKey(privateKey, response, rpcUrl) {
  const privateKeyHex = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`;
  const account = privateKeyToAccount(privateKeyHex);
  const txRequest = response.json; // { to, data, value, gas, nonce, chainId, maxFeePerGas, maxPriorityFeePerGas, type }

  const walletClient = createWalletClient({
    chain: base,
    account,
    transport: http(rpcUrl),
  });

  const prepared = await walletClient.prepareTransactionRequest(txRequest);
  return walletClient.signTransaction(prepared);
}

// -----------------------------------------------------------------------------
// 3. With private key — sign only (no RPC), from encoded, using viem/accounts
// -----------------------------------------------------------------------------
async function signRawTransactionWithPrivateKeyNoRpc(privateKey, response) {
  const privateKeyHex = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`;
  const rawTxHex = response.encoded.startsWith('0x') ? response.encoded : `0x${response.encoded}`;
  const transaction = parseTransaction(rawTxHex);

  const signedHex = await signTransaction({
    privateKey: privateKeyHex,
    transaction,
  });
  return signedHex;
}
import { createWalletClient, custom, http, parseTransaction } from 'viem';
import { privateKeyToAccount, signTransaction } from 'viem/accounts';
import { base } from 'viem/chains';


// Example API response shape
// response = { json: { domain, types, primaryType, message }, encoded: "0x..." }

// -----------------------------------------------------------------------------
// 4. Via provider (frontend) — use response.encoded (raw hex), wallet signs
// -----------------------------------------------------------------------------
async function signRawTransactionViaProvider(provider, response) {
  const rawTxHex = response.encoded.startsWith('0x') ? response.encoded : `0x${response.encoded}`;
  const transaction = parseTransaction(rawTxHex);

  const walletClient = createWalletClient({
    chain: base,
    transport: custom(provider),
  });

  const [address] = await walletClient.getAddresses();
  if (!address) throw new Error('Wallet not connected');

  const request = await walletClient.prepareTransactionRequest({
    ...transaction,
    gas: transaction.gas,
    account: address,
  });

  return walletClient.signTransaction(request);
}

// -----------------------------------------------------------------------------
// 5. Via provider (frontend) — use response.json (transaction object)
// -----------------------------------------------------------------------------
async function signTransactionObjectViaProvider(provider, response) {
  const walletClient = createWalletClient({
    chain: base,
    transport: custom(provider),
  });

  const [address] = await walletClient.getAddresses();
  if (!address) throw new Error('Wallet not connected');

  const request = await walletClient.prepareTransactionRequest({
    ...response.json,
    account: address,
  });

  return walletClient.signTransaction(request);
}

3. Broadcast signed final transaction

NB: required for final transaction type only.

To submit the final transaction to the network, send a POST request to /api/defi/v1/transactions/steps/broadcast.

Example request:

curl --request POST \
  --url 'https://edge.p2p.org/api/defi/v1/transactions/steps/broadcast' \
  --header 'accept: application/json' \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '{
      "chain": "base",
      "protocol": "morpho",
      "strategy": "Steakhouse USDC",
      "signedPayload": "0x…"
    }'
  • signedTransaction — signed final transaction in the hexadecimal format which needs to be broadcasted to the network.

Example response:

{
  "error": null,
  "result": {
    "transactionHash": "0x1234567890abcdef..."
  },
  "meta": {
    "time": 0,
    "requestId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }
}
  • transactionHash — hash of the final transaction.
  • time — request execution time in ms.
  • requestId — unique identifier of the request.

What's next?