@robertprp/intents-sdk
Version:
Shogun Network Intent-based cross-chain swaps SDK
248 lines (206 loc) • 8.33 kB
text/typescript
import { privateKeyToAccount } from 'viem/accounts';
import type { EVMConfig } from '../../config.js';
import { BaseSDK } from '../sdk.js';
import { EVMIntentProvider } from './intent-provider.js';
import { type Address, type Hex } from 'viem';
import {
PERMIT2_ADDRESS,
CROSS_CHAIN_GUARD_ADDRESSES,
SINGLE_CHAIN_GUARD_ADDRESSES,
MAX_UINT_256,
} from '../../constants.js';
import type { ExtraTransfer } from '../orders/common.js';
import type { CrossChainOrder } from '../orders/cross-chain.js';
import type { SingleChainOrder } from '../orders/single-chain.js';
import type { CrossChainOrderPrepared, SingleChainOrderPrepared } from '../../types/intent.js';
import type { ApiUserOrders } from '../../types/api.js';
import { fetchJWTToken, fetchSiweMessage } from '../../auth/siwe.js';
import { fetchUserOrders } from '../orders/api/fetch.js';
type CancelSingleChainOrderParams = {
orderId: string;
user: string;
tokenIn: string;
amountIn: bigint;
requestedOutput: ExtraTransfer;
extraTransfers: ExtraTransfer[];
encodedExternalCallData: string;
deadline: number;
nonce: bigint;
};
type CancelCrossChainOrderParams = {
orderId: string;
user: string;
tokenIn: string;
amountIn: bigint;
srcChainId: number;
deadline: number;
minStablecoinsAmount: bigint;
executionDetailsHash: string;
nonce: bigint;
};
/**
* Handles EVM-specific aspects of cross-chain swaps for Ethereum-compatible chains:
*/
export class EVMSDK extends BaseSDK {
private readonly config: EVMConfig;
private readonly evmIntentProvider: EVMIntentProvider;
private token?: string;
constructor(config: EVMConfig) {
super();
this.config = config;
this.evmIntentProvider = new EVMIntentProvider(config);
}
public async cancelCrossChainOrder(params: CancelCrossChainOrderParams): Promise<string> {
const chainId = this.config.chainId;
const auctioneerAddress = CROSS_CHAIN_GUARD_ADDRESSES[chainId] as Hex;
const userAddress = (await this.getUserAddress()) as Hex;
const auctioneerContract = this.evmIntentProvider.provider.getCrossChainAuctioneerContract(auctioneerAddress);
const permit2Contract = this.evmIntentProvider.provider.getPermit2Contract(PERMIT2_ADDRESS[chainId]);
const orderIdHex = params.orderId as Hex;
let [initialized, deactivated] = await auctioneerContract.read.orderData([orderIdHex]);
let txHash: Hex;
if (initialized) {
if (deactivated) {
throw new Error('Order is already deactivated');
}
const orderInfo = {
user: params.user as Hex,
tokenIn: params.tokenIn as Hex,
srcChainId: params.srcChainId,
deadline: params.deadline,
amountIn: BigInt(params.amountIn),
minStablecoinsAmount: BigInt(params.minStablecoinsAmount),
executionDetailsHash: params.executionDetailsHash as Hex,
nonce: BigInt(params.nonce!),
};
const tx = await auctioneerContract.write.cancelOrder([orderInfo]);
txHash = tx;
} else {
const nonce = BigInt(params.nonce);
let nonceWordPos = nonce >> 8n;
let nonceBitPos = nonce - nonceWordPos * 256n;
let mask = 1n << nonceBitPos;
const currentNonceBitmap = await permit2Contract.read.nonceBitmap([userAddress, nonceWordPos]);
if ((currentNonceBitmap & (1n << nonceBitPos)) !== 0n) {
throw new Error('Nonce is already invalidated');
}
const tx = await permit2Contract.write.invalidateUnorderedNonces([nonceWordPos, mask]);
txHash = tx;
}
return txHash;
}
public async cancelSingleChainOrder(params: CancelSingleChainOrderParams): Promise<string> {
const chainId = this.config.chainId;
const auctioneerAddress = SINGLE_CHAIN_GUARD_ADDRESSES[chainId] as Hex;
const userAddress = (await this.getUserAddress()) as Hex;
const auctioneerContract = this.evmIntentProvider.provider.getSingleChainAuctioneerContract(auctioneerAddress);
const orderHash = params.orderId as Hex;
const wasManuallyInitialized = await auctioneerContract.read.orderManuallyInitialized([orderHash]);
let txHash: Hex;
if (wasManuallyInitialized) {
txHash = await auctioneerContract.write.cancelManuallyCreatedOrder([
{
amountIn: params.amountIn,
tokenIn: params.tokenIn as Hex,
deadline: params.deadline as number,
nonce: params.nonce,
encodedExternalCallData: params.encodedExternalCallData as Hex,
extraTransfers: params.extraTransfers.map((transfer) => {
return {
amount: transfer.amount,
receiver: transfer.receiver as Hex,
token: transfer.token as Hex,
};
}),
requestedOutput: {
amount: params.requestedOutput.amount,
receiver: params.requestedOutput.receiver as Hex,
token: params.requestedOutput.token as Hex,
},
user: params.user as Hex,
},
]);
} else {
const permit2Contract = this.evmIntentProvider.provider.getPermit2Contract(PERMIT2_ADDRESS[chainId]);
const nonce = BigInt(params.nonce);
const nonceWordPos = nonce >> 8n;
const nonceBitPos = nonce - nonceWordPos * 256n;
const mask = 1n << nonceBitPos;
// Check if nonce is already invalidated
const currentNonceBitmap = await permit2Contract.read.nonceBitmap([userAddress, nonceWordPos]);
if ((currentNonceBitmap & (1n << nonceBitPos)) !== 0n) {
throw new Error('Nonce is already invalidated');
}
txHash = await permit2Contract.write.invalidateUnorderedNonces([nonceWordPos, mask]);
}
return txHash;
}
public async authenticate(token?: string): Promise<string> {
const chainId = this.config.chainId;
const wallet = await this.getUserAddress();
const response = await fetchSiweMessage({
chainId,
wallet,
});
const message = response.data!;
// Sign the message using the wallet client
const signature = await this.evmIntentProvider.provider.walletClient.signMessage({
message,
});
const jwt = await fetchJWTToken(
{
message,
signature,
},
token,
);
const newToken = jwt.data!;
return newToken;
}
public setToken(token: string) {
this.token = token;
return this;
}
public override async getOrders(): Promise<ApiUserOrders> {
if (!this.token) {
throw new Error('No token provided');
}
const orders = await fetchUserOrders(this.token);
return orders;
}
/**
* Gets the user's Ethereum address derived from their private key
* @returns Promise resolving to the user's Ethereum address as a 0x-prefixed string
*/
public async getUserAddress(): Promise<string> {
return privateKeyToAccount(this.config.privateKey).address;
}
protected async approveAllowanceIfNeeded(tokenAddress: Address, amount: bigint): Promise<void> {
const permit2Address = PERMIT2_ADDRESS[this.config.chainId];
const ERC20Contract = this.evmIntentProvider.provider.getERC20Contract(tokenAddress);
const userAddress = (await this.getUserAddress()) as Address;
const allowance = await ERC20Contract.read.allowance([userAddress, permit2Address]);
if (allowance < amount) {
await ERC20Contract.write.approve([permit2Address, MAX_UINT_256]);
}
}
/**
* Prepares an EVM order for submission
*
* This method:
* 1. Validates token balances and allowances
* 2. Creates and signs the EIP-712 typed data for Permit2
* 3. Prepares the order for submission to the auctioneer
*
* @param order The validated order to prepare
* @returns Promise resolving to a prepared order with EVM-specific signature data
*/
protected override async prepareCrossChainOrder(order: CrossChainOrder): Promise<CrossChainOrderPrepared> {
await this.approveAllowanceIfNeeded(order.sourceTokenAddress as Address, order.sourceTokenAmount);
return this.evmIntentProvider.prepareCrossChainOrder(order);
}
protected async prepareSingleChainOrder(order: SingleChainOrder): Promise<SingleChainOrderPrepared> {
await this.approveAllowanceIfNeeded(order.tokenIn as Address, order.amountIn);
return this.evmIntentProvider.prepareSingleChainOrder(order);
}
}