Skip to content

Commit

Permalink
Crosschainswaps V3 (#71)
Browse files Browse the repository at this point in the history
* feat: added relay bridging support

* fix: ocd and lint

* fix: temp version

* fix: testing on staging

* fix: lint

* feat: moving to another file for testing

* feat: refactored a bit

* fix: tests were not running

* fix: error message

* fix: should not change these

* feat: new field

* fix: should not replace to address

* feat: tests and lsat details

* fix: doc for erc20 transfer example
  • Loading branch information
fringlesinthestreet authored May 2, 2024
1 parent a8f7070 commit a1071ec
Show file tree
Hide file tree
Showing 8 changed files with 386 additions and 11 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.idea
.idea/**
sdk/*.tgz
node_modules/
7 changes: 5 additions & 2 deletions sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.18.0",
"version": "0.19.0",
"name": "@rainbow-me/swaps",
"license": "GPL-3.0",
"main": "dist/index.js",
Expand All @@ -13,7 +13,7 @@
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"build": "tsdx build --ignore-pattern tests/*",
"test": "tsdx test --passWithNoTests",
"lint": "eslint . --ext js,ts,jsx,tsx",
"prepare": "tsdx build",
Expand Down Expand Up @@ -83,5 +83,8 @@
"**/glob-parent": "5.1.2",
"**/ws": "7.4.6",
"**/ansi-regex": "5.0.1"
},
"jest": {
"testEnvironment": "node"
}
}
52 changes: 45 additions & 7 deletions sdk/src/quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ import {
PERMIT_EXPIRATION_TS,
RAINBOW_ROUTER_CONTRACT_ADDRESS,
RAINBOW_ROUTER_CONTRACT_ADDRESS_ZORA,
SOCKET_GATEWAY_CONTRACT_ADDRESSESS,
WRAPPED_ASSET,
} from './utils/constants';
import { signPermit } from './utils/permit';
import { getReferrerCode } from './utils/referrer';
import {
extractDestinationAddress,
sanityCheckAddress,
} from './utils/sanity_check';

/**
* Function to get the rainbow router contract address based on the chainId
Expand Down Expand Up @@ -148,7 +151,7 @@ const buildRainbowCrosschainQuoteUrl = ({
swapType: SwapType.crossChain,
toChainId: String(toChainId),
});
return `${API_BASE_URL}/v1/quote?bridgeVersion=2&` + searchParams.toString();
return `${API_BASE_URL}/v1/quote?bridgeVersion=3&` + searchParams.toString();
};

/**
Expand Down Expand Up @@ -278,7 +281,9 @@ export const getQuote = async (
* @param {BigNumberish} params.sellAmount
* @param {number} params.slippage
* @param {boolean} params.refuel
* @returns {Promise<CrosschainQuote | null>}
* @returns {Promise<CrosschainQuote | QuoteError | null>} returns error in case the request failed or the
* destination address is not consistent with the SDK's
* stored destination address
*/
export const getCrosschainQuote = async (
params: QuoteParams
Expand Down Expand Up @@ -316,8 +321,24 @@ export const getCrosschainQuote = async (
}

const quoteWithRestrictedAllowanceTarget = quote as CrosschainQuote;
quoteWithRestrictedAllowanceTarget.allowanceTarget =
SOCKET_GATEWAY_CONTRACT_ADDRESSESS.get(chainId);
try {
const { expectedAddress, shouldOverride } = sanityCheckAddress(
quoteWithRestrictedAllowanceTarget.source,
quoteWithRestrictedAllowanceTarget.chainId,
quoteWithRestrictedAllowanceTarget.allowanceTarget
);
if (shouldOverride) {
quoteWithRestrictedAllowanceTarget.allowanceTarget = expectedAddress;
}
} catch (e) {
return {
error: true,
message:
e instanceof Error
? e.message
: `unexpected error happened while checking crosschain quote's address: ${quoteWithRestrictedAllowanceTarget.allowanceTarget}`,
} as QuoteError;
}

return quoteWithRestrictedAllowanceTarget;
};
Expand Down Expand Up @@ -490,7 +511,15 @@ export const fillCrosschainQuote = async (
): Promise<Transaction> => {
const { data, from, value } = quote;

const to = SOCKET_GATEWAY_CONTRACT_ADDRESSESS.get(quote.fromChainId);
let to = quote.to;
const { expectedAddress, shouldOverride } = sanityCheckAddress(
quote.source,
quote.fromChainId,
extractDestinationAddress(quote)
);
if (shouldOverride) {
to = expectedAddress;
}

let txData = data;
if (referrer) {
Expand Down Expand Up @@ -589,7 +618,16 @@ export const getCrosschainQuoteExecutionDetails = (
provider: StaticJsonRpcProvider
): CrosschainQuoteExecutionDetails => {
const { from, data, value } = quote;
const to = SOCKET_GATEWAY_CONTRACT_ADDRESSESS.get(quote.fromChainId);

let to = quote.to;
const { expectedAddress, shouldOverride } = sanityCheckAddress(
quote.source,
quote.fromChainId,
extractDestinationAddress(quote)
);
if (shouldOverride) {
to = expectedAddress;
}

return {
method: provider.estimateGas({
Expand Down
6 changes: 6 additions & 0 deletions sdk/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ export enum ChainId {
export enum Source {
Aggregator0x = '0x',
Aggregator1inch = '1inch',
AggregatorRainbow = 'rainbow',
// DEPRECATED: Use Aggregator1inch instead
Aggregotor1inch = '1inch',

// Crosschain
CrosschainAggregatorSocket = 'socket',
CrosschainAggregatorRelay = 'relay',
}

export enum SwapType {
Expand Down Expand Up @@ -234,6 +239,7 @@ export interface CrosschainQuote extends Quote {
allowanceTarget?: string;
routes: SocketRoute[];
refuel: SocketRefuelData | null;
no_approval: boolean | undefined;
}

export interface TransactionOptions {
Expand Down
6 changes: 6 additions & 0 deletions sdk/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export const SOCKET_GATEWAY_CONTRACT_ADDRESSESS = new Map([
[ChainId.blast, '0x3a23F943181408EAC424116Af7b7790c94Cb97a5'],
]);

// RELAY_LINK_BRIDGING_RELAYER_ADDRESS is the EOA used by relay link as relayer on all chains
export const RELAY_LINK_BRIDGING_RELAYER_ADDRESS =
'0xf70da97812CB96acDF810712Aa562db8dfA3dbEF';

export const ERC20_TRANSFER_SIGNATURE = `0xa9059cbb`;

export type MultiChainAsset = {
[key: string]: EthereumAddress;
};
Expand Down
167 changes: 167 additions & 0 deletions sdk/src/utils/sanity_check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { ChainId, CrosschainQuote, Source } from '../types';
import {
ERC20_TRANSFER_SIGNATURE,
ETH_ADDRESS,
RELAY_LINK_BRIDGING_RELAYER_ADDRESS,
SOCKET_GATEWAY_CONTRACT_ADDRESSESS,
} from './constants';

/**
* Sanity checks the quote's returned address against the expected address stored in the SDK.
* This function ensures the integrity and correctness of the destination address provided by the quote source.
*
* @param quoteSource The aggregator used for the quote.
* @param chainID The origin network chain ID for the quote.
* @param assertedAddress The destination address provided by the quote.
* @returns {string, boolean} The destination address stored in the SDK for the provided (source, chainID) combination.
* And if it should be overridden in the quote.
* @throws {Error} Throws an error if any of the following conditions are met:
* - The quote's destination address is undefined.
* - No destination address is defined in the SDK for the provided (source, chainID) combination.
* - The provided quote's destination address does not case-insensitively match the SDK's stored destination address.
*/
export function sanityCheckAddress(
quoteSource: Source | undefined,
chainID: ChainId,
assertedAddress: string | undefined
): {
expectedAddress: string;
shouldOverride: boolean;
} {
if (assertedAddress === undefined || assertedAddress === '') {
throw new Error(
`quote's destination addresses must be defined (API Response)`
);
}
const { expectedAddress, shouldOverride } = getExpectedDestinationAddress(
quoteSource,
chainID
);
if (expectedAddress === undefined || expectedAddress === '') {
throw new Error(
`expected source ${quoteSource}'s destination address on chainID ${chainID} must be defined (Swap SDK)`
);
}
if (expectedAddress.toLowerCase() !== assertedAddress?.toLowerCase()) {
throw new Error(
`source ${quoteSource}'s destination address '${assertedAddress}' on chainID ${chainID} is not consistent, expected: '${expectedAddress}'`
);
}
return { expectedAddress, shouldOverride };
}

/**
* Retrieves the destination address from a cross-chain quote object, returning undefined
* when the quote source is not known or the quote does not contain a valid destination address.
*
* @param quote The cross-chain quote object returned by the API.
*
* @returns The destination address as a string if available.
* Returns undefined if the quote does not properly specify a destination.
*
* @example
* // Example for a quote from socket
* const quoteSocket = {
* to: '0x1234567890123456789012345678901234567890',
* data: '0x...',
* sellTokenAddress: '0x...'
* };
* console.log(getToAddressFromCrosschainQuote(Source.CrosschainAggregatorSocket, quoteSocket));
* // Output: '0x1234567890123456789012345678901234567890'
*
* // Example for a quote from CrosschainAggregatorRelay where the sell token is ETH
* const quoteRelayETH = {
* to: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef',
* data: '0x...',
* sellTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
* };
* console.log(getToAddressFromCrosschainQuote(Source.CrosschainAggregatorRelay, quoteRelayETH));
* // Output: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef'
*
* // Example for a quote from CrosschainAggregatorRelay where the sell token is not ETH
* const quoteRelayERC20 = {
* to: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef',
* data: '0xa9059cbb000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef...',
* sellTokenAddress: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef'
* };
* console.log(getToAddressFromCrosschainQuote(Source.CrosschainAggregatorRelay, quoteRelayERC20));
* // Output: '0xf70da97812cb96acdf810712aa562db8dfa3dbef' (assuming the call data was a ERC20 transfer)
*/
export function extractDestinationAddress(
quote: CrosschainQuote
): string | undefined {
const quoteSource = quote.source;
const validQuoteSource = quoteSource !== undefined;
if (validQuoteSource && quoteSource === Source.CrosschainAggregatorSocket) {
return quote.to;
}
if (validQuoteSource && quoteSource === Source.CrosschainAggregatorRelay) {
if (quote.sellTokenAddress?.toLowerCase() === ETH_ADDRESS.toLowerCase()) {
return quote.to;
}
return decodeERC20TransferToData(quote.data);
}
return undefined;
}

/**
* Decodes the ERC-20 token transfer data from a transaction's input data.
* This function expects the input data to start with the ERC-20 transfer method ID (`0xa9059cbb`),
* followed by the 64 hexadecimal characters for the destination address and 64 hexadecimal characters
* for the transfer amount. The function will check and parse the input data, extracting the recipient's address
*
* The method assumes the data is properly formatted and begins with the correct method ID.
* If the data does not conform to these expectations, the function will return an 'undefined' object.
*
* @param data The hex encoded input data string from an ERC-20 transfer transaction. This string
* should include the method ID followed by the encoded parameters (address and amount).
*
* @returns { string | undefined } The destination address. If any error happens.
* Returns 'undefined' if it could not decode the call data.
*/
export function decodeERC20TransferToData(
data: string | undefined
): string | undefined {
if (!data?.startsWith(ERC20_TRANSFER_SIGNATURE)) {
return undefined;
}
const paramsData = data.slice(ERC20_TRANSFER_SIGNATURE.length);
if (paramsData.length < 64 * 2) {
return undefined;
}
return `0x${paramsData.slice(0, 64).replace(/^0+/, '')}`;
}

/**
* Retrieves the destination address stored in the SDK corresponding to the specified aggregator and chain ID.
*
* @param quoteSource The aggregator used for the quote.
* @param chainID The origin network chain ID for the quote.
* @returns {string | undefined, boolean} The destination address stored in the SDK for the provided (source, chainID)
* combination and if we need to overwrite it on the quote.
* Returns `undefined` if there is no address for the specified combination.
*/
export function getExpectedDestinationAddress(
quoteSource: Source | undefined,
chainID: ChainId
): {
expectedAddress: string | undefined;
shouldOverride: boolean;
} {
const validSource = quoteSource !== undefined;
if (validSource && quoteSource === Source.CrosschainAggregatorSocket) {
return {
expectedAddress: SOCKET_GATEWAY_CONTRACT_ADDRESSESS.get(chainID),
shouldOverride: true,
};
} else if (validSource && quoteSource === Source.CrosschainAggregatorRelay) {
return {
expectedAddress: RELAY_LINK_BRIDGING_RELAYER_ADDRESS,
shouldOverride: false,
};
}
return {
expectedAddress: undefined,
shouldOverride: false,
};
}
Loading

0 comments on commit a1071ec

Please sign in to comment.