DeFi API Approval code examples
Signing an Intent for EVM
Permit2 lets the user lock tokens and generate a signature to authorize a swap. Tokens stay secure until the swap is executed, either on the same chain or across chains. The resolver uses this signature to move funds into escrow and complete the swap.
Script to create a Permit2 signature:
import { privateKeyToAccount } from 'viem/accounts';
import { ChangellyDeFiClient } from '@changelly/defi-api-sdk-ts';
import { preparePermit2Approval } from '@changelly/defi-api-sdk-ts';
import {
type DepositType,
type Permit2ApprovalToSign,
type SignedApproval,
SwapType,
} from '@changelly/defi-api-sdk-ts';
async function main() {
const apiKey = process.env['API_KEY'];
const privateKey = process.env['PRIVATE_KEY'];
const to_address = process.env['DESTINATION_ADDRESS'];
if (!apiKey) {
throw new Error('API_KEY environment variable is not set');
}
if (!privateKey) {
throw new Error('PRIVATE_KEY environment variable is not set');
}
if (!to_address) {
throw new Error('DESTINATION_ADDRESS environment variable is not set');
}
const account = privateKeyToAccount(privateKey as `0x${string}`);
// Initialize the Changelly DeFi API client with the API key
const client = new ChangellyDeFiClient(
'https://dex-api.changelly.com/affiliate',
apiKey
);
// Example parameters for getting a quote
const from_network_id = 1; // Ethereum mainnet
const from_token = '0xc02aaa39********83c756cc2'; // WETH
const to_network_id = 2; // Tron mainnet
const to_token = 'TR7NHq********8otSzgjLj6t'; // USDT on Tron
const amount = 0.001; // Amount in WETH
const slippage_bps = 50n; // 0.5% slippage
const swapType = SwapType.Standard;
const depositType: DepositType = 'escrowed';
// Get a quote for the swap
console.log('Getting quote...');
const quote = await client.getQuote(
from_network_id,
from_token,
to_network_id,
to_token,
amount,
slippage_bps,
swapType,
null, // retail user id
depositType,
);
console.log('Quote received:', quote);
// Set user addresses
const from_address = account.address;
const to_address = to_address;
const refund_address = account.address;
// Create an intent based on the quote
console.log('Creating intent...');
const intentData = await client.createIntent(
quote.id,
null, // userSourcePublicKey (null for EVM)
from_address,
to_address,
refund_address,
);
console.log('Intent created:', intentData);
// Prepare the permit2 approval for signing
const permit2Approval = preparePermit2Approval(
intentData.params_to_sign as Permit2ApprovalToSign,
quote,
from_address,
intentData.deadline_secs,
);
console.log('Permit2 approval prepared:', permit2Approval);
const signature = await account.signTypedData(permit2Approval);
const signedApproval: SignedApproval = {
type: 'permit2',
signed_data: signature,
};
// Add the approval to the intent
console.log('Adding approval...');
await client.addApproval(signedApproval, intentData.intent_id);
console.log('Approval added');
}
await main()
.then(() => console.log('Example completed'))
.catch(console.error);
Signing an Intent for Tron
Permit2 on Tron lets the user lock tokens and generate a signature to authorize a swap. Tokens remain secure until the resolver executes the swap, either on Tron or across chains, using the signature to move funds into escrow.
Script to create a Permit2 signature:
import { TronWeb } from 'tronweb';
import { ChangellyDeFiClient } from '@changelly/defi-api-sdk-ts';
import { preparePermit2Approval } from '@changelly/defi-api-sdk-ts';
import {
type DepositType,
type Permit2ApprovalToSign,
type SignedApproval,
SwapType,
} from '@changelly/defi-api-sdk-ts';
async function main() {
const apiKey = process.env['API_KEY'];
const privateKey = process.env['PRIVATE_KEY'];
const to_address = process.env['DESTINATION_ADDRESS'];
if (!apiKey) {
throw new Error('API_KEY environment variable is not set');
}
if (!privateKey) {
throw new Error('PRIVATE_KEY environment variable is not set');
}
if (!to_address) {
throw new Error('DESTINATION_ADDRESS environment variable is not set');
}
const tronWeb = new TronWeb({
fullHost: 'https://api.trongrid.io',
privateKey: privateKey,
});
// Initialize the Changelly DeFi API client with the API key
const client = new ChangellyDeFiClient(
'https://dex-api.changelly.com/affiliate',
apiKey
);
// Example parameters for getting a quote
const from_network_id = 2; // Tron mainnet
const from_token = 'TR7********Lj6t'; // USDT on Tron
const to_network_id = 1; // Ethereum mainnet
const to_token = '0xc02a********c756cc2'; // WETH
const amount = 2; // Amount in USDT
const slippage_bps = 50n; // 0.5% slippage
const swapType = SwapType.Standard;
const depositType: DepositType = 'escrowed';
// Get a quote for the swap
console.log('Getting quote...');
const quote = await client.getQuote(
from_network_id,
from_token,
to_network_id,
to_token,
amount,
slippage_bps,
swapType,
null, // retail user id
depositType,
);
console.log('Quote received:', quote);
// Set user addresses
const from_address = tronWeb.address.fromPrivateKey(privateKey);
const to_address = to_address;
const refund_address = tronWeb.address.fromPrivateKey(privateKey);
if (!from_address || !refund_address) {
throw new Error('User source address or refund address is invalid');
}
// Create an intent based on the quote
console.log('Creating intent...');
const intentData = await client.createIntent(
quote.id,
null,
from_address,
to_address,
refund_address,
);
console.log('Intent created:', intentData);
// Prepare the permit2 approval for signing
const permit2Approval = preparePermit2Approval(
intentData.params_to_sign as Permit2ApprovalToSign,
quote,
from_address,
intentData.deadline_secs,
);
console.log('Permit2 approval prepared:', permit2Approval);
const signature = tronWeb.trx.signTypedData(
permit2Approval.domain,
permit2Approval.types,
permit2Approval.message,
privateKey,
);
const signedApproval: SignedApproval = {
type: 'permit2',
signed_data: signature,
};
// Add the approval to the intent
console.log('Adding approval...');
await client.addApproval(signedApproval, intentData.intent_id);
console.log('Approval added');
}
await main()
.then(() => console.log('Example completed'))
.catch(console.error);
Signing an Intent for Solana
Versioned transaction cosigning allows the user to authorize a swap by signing a prebuilt Solana transaction that encodes the exact execution parameters. The transaction is generated from a quote, signed locally by the user while preserving required signature order, and returned as an approval for execution. Funds remain secure until the resolver executes the swap according to the authorized transaction.
Script to cosign a versioned transaction on Solana:
import { Keypair, VersionedTransaction } from '@solana/web3.js';
import bs58 from 'bs58';
import { ChangellyDeFiClient } from '@changelly/defi-api-sdk-ts';
import {
type CosignApprovalToSign,
type DepositType,
type SignedApproval,
SwapType,
} from '@changelly/defi-api-sdk-ts';
async function main() {
const apiKey = process.env['API_KEY'];
const privateKey = process.env['PRIVATE_KEY'];
const to_address = process.env['DESTINATION_ADDRESS'];
if (!apiKey) {
throw new Error('API_KEY environment variable is not set');
}
if (!privateKey) {
throw new Error('PRIVATE_KEY environment variable is not set');
}
if (!to_address) {
throw new Error('DESTINATION_ADDRESS environment variable is not set');
}
const secretKeyBytes = bs58.decode(privateKey);
const keypair = Keypair.fromSecretKey(secretKeyBytes);
// Initialize the Changelly DeFi API client with the API key
const client = new ChangellyDeFiClient(
'https://dex-api.changelly.com/affiliate',
apiKey
);
// Example parameters for getting a quote
const from_network_id = 4; // Solana mainnet
const from_token = 'So11111111111111111111111111111111111111112'; // WSOL
const to_network_id = 1; // Ethereum mainnet
const to_token = '0xc02aaa3********9083c756cc2'; // WETH
const amount = 0.01; // Amount in WSOL
const slippage_bps = 50n; // 0.5% slippage
const swapType = SwapType.Standard;
const depositType: DepositType = 'escrowed';
// Get a quote for the swap
console.log('Getting quote...');
const quote = await client.getQuote(
from_network_id,
from_token,
to_network_id,
to_token,
amount,
slippage_bps,
swapType,
null, // retail user id
depositType,
);
console.log('Quote received:', quote);
// Set user addresses
const from_address = keypair.publicKey.toBase58();
const to_address = to_address;
const refund_address = keypair.publicKey.toBase58();
// Create an intent based on the quote
console.log('Creating intent...');
const intentData = await client.createIntent(
quote.id,
null, // userSourcePublicKey (null for Solana)
from_address,
to_address,
refund_address,
);
console.log('Intent created:', intentData);
const { transaction } = intentData.params_to_sign as CosignApprovalToSign;
// Decode hex-encoded transaction from the backend
const bytes = Uint8Array.from(Buffer.from(transaction, 'hex'));
// Construct VersionedTransaction
const tx = VersionedTransaction.deserialize(bytes);
// Save old signatures that might be overwritten by `tx.sign`
const signatures = [...tx.signatures];
tx.sign([keypair]);
// Adjust signature positions: user's signature must appear at position 1,
// resolver's signature must appear at position 0,
// and optionally backend signature must appear at position 2.
tx.signatures[0] = signatures[0];
if (tx.signatures[2] !== undefined) {
tx.signatures[2] = signatures[2];
}
const signedApproval: SignedApproval = {
type: 'cosign',
signed_data: {
transaction: Buffer.from(tx.serialize()).toString('hex'),
user_address: keypair.publicKey.toBase58(),
},
};
// Add the approval to the intent
console.log('Adding approval...');
await client.addApproval(signedApproval, intentData.intent_id);
console.log('Approval added');
}
await main()
.then(() => console.log('Example completed'))
.catch(console.error);
Signing an Intent for Bitcoin
PSBT cosigning allows a user to authorize a Bitcoin HTLC deposit by signing only the inputs they control. The transaction is generated by the protocol, and the user signs it locally while preserving any existing partial signatures. The signed PSBT is returned as an approval, keeping funds secure until the resolver executes the swap according to the authorized transaction parameters.
PSBT Signing Process:
The signing procedure consists of the following steps:
- Parse the Base64-encoded PSBT into a structured PSBT object.
- Decode the private key from its serialized format (e.g., hex) into a usable signing key.
- Iterate over the specified input indices that require signing.
- For each input, obtain the referenced UTXO information directly from the PSBT (amount, script, and Taproot-related data if applicable).
- For each selected input, compute the appropriate signature hash and produce a Taproot (Schnorr) witness signature.
- Insert the generated signatures into the corresponding input fields without modifying other transaction data.
- Serialize the updated PSBT back into Base64 format for further processing or submission.
Script to sign a PSBT with Changelly DeFi Golang SDK:
package main
import (
"fmt"
"log"
"github.com/changelly/defi-api-sdk-go/crypto/bitcoin"
"github.com/changelly/defi-api-sdk-go/types"
)
func main() {
var (
signerPrivateKeyHex = os.Getenv("PRIVATE_KEY")
// NOTE: mocking createIntent response since psbt and inputs are part of it.
approvalToSign = types.ApprovalToSign{
ApprovalMechanism: types.ApprovalToSignTypeHtlc,
Htlc: &types.ApprovalToSignHtlc{
Psbt: "your-psbt-base64", // Replace with your base64-encoded PSBT.
Inputs: []int{0, 1, 2}, // Indices of the inputs you need to sign.
},
}
)
// 1. Initialize the signer
signer, err := bitcoin.NewSigner(signerPrivateKeyHex)
if err != nil {
log.Fatalf("failed to create signer: %v", err)
}
// 2. Sign the deposit transaction
signedPsbtBase64, err := bitcoin.SignDepositTx(
signer,
approvalToSign.Htlc.Psbt,
approvalToSign.Htlc.Inputs,
)
if err != nil {
log.Fatalf("failed to sign deposit tx: %v", err)
}
// 3. Use the signed PSBT as an approval.
// NOTE: signedPsbtBase64 should be provided back to the protocol
// via `add-approval` API call.
fmt.Println("Signed PSBT Base64:", signedPsbtBase64)
intentApproval = types.NewHtlcIntentApproval(signedPsbtBase64)
// Propagate approval to the Changelly DeFi API and fetch the status.
}
Signing with TypeScript:
Most wallets provide a signPsbt method that handles all cryptographic operations internally. Using Sats Connect, you can sign a Base64-encoded PSBT for the inputs you control and receive a signed PSBT ready to return to the protocol.
import { request, RpcErrorCode } from "sats-connect";
/**
* Signs a Base64 PSBT (or with another Sats Connect compatible wallet).
*
* @param psbtBase64 Base64-encoded PSBT from the API response
* @param signInputs Map: address -> input indexes to sign, from the API response
* @returns signed PSBT (Base64)
*/
export async function signPsbt(
psbtBase64: string,
signInputs: Record<string, number[]>
): Promise<string> {
const response = await request("signPsbt", {
psbt: psbtBase64,
signInputs,
broadcast: false,
});
if (response.status === "success") {
return response.result.psbt;
}
throw new Error(
`PSBT signing failed: ${response.error.message ?? "Unknown error"}`
);
}