Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: getSwap #503

Merged
merged 8 commits into from
Jun 11, 2024
Merged

feat: getSwap #503

merged 8 commits into from
Jun 11, 2024

Conversation

0xAlec
Copy link
Contributor

@0xAlec 0xAlec commented Jun 10, 2024

What changed? Why?

  • add getSwap
  • add SwapParams for both getQuote and getSwap (shared params for all core/swap utils)
  • add types and Transaction interface with a helper method for injecting nonce and EIP-1559 gas values

Notes to reviewers

  • we do not support gasPrice (i.e. pre-EIP-1559 transactions)
  • we may need to slightly tweak the data shape/parameters to support Wagmi's useSendTransaction hook - this was built with usage in Viem's sendTransaction in mind. (next PR)

How has it been tested?

Checking for a warning

const swap = await getSwap({
  fromAddress: account.address, // This address doesn't have ETH to pay gas, so the simulation will fail
  from: eth,
  to: degen,
  amount: '1',
});


if (swap.warning) {
  // Display the warning to the user
  console.log(swap.warning)
  // {
  //   type: 'warning',
  //   message: 'This transaction has a very high likelihood of failing if submitted',
  //   description: 'failed with 600000000 gas: insufficient funds for gas * price + value: address 0x6Cd01c0F55ce9E0Bf78f5E90f72b4345b16d515d have 0 want 100000000000000000000'
  // }
}

Example of a successful ETH -> DEGEN swap:

const eth: Token = {
  name: 'ETH',
  address: '',
  symbol: 'ETH',
  decimals: 18,
  image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
  chainId: 8453,
};

const degen: Token = {
  name: 'DEGEN',
  address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed',
  symbol: 'DEGEN',
  decimals: 18,
  image:
 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm',
  chainId: 8453,
};

const swap = await getSwap({
  fromAddress: account.address,
  from: eth,
  to: degen,
  amount: '0.0001',
});

// Build the transaction
const nonce = await publicClient.getTransactionCount({ address: account.address });
const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas();

const params: TransactionParams = { nonce, maxFeePerGas, maxPriorityFeePerGas };

const tx = swap.transaction.withParams(params);

// Sign the transaction
const serializedTransaction = await account.signTransaction(tx);

// Make the trade
const txHash = await publicClient.sendRawTransaction({ serializedTransaction });
console.log(`Transaction sent with hash: ${txHash}`);

Swap - https://basescan.org/tx/0xca2a5a324b3c011f35a9e5e182c2f9e11b1714b739093b66b7bc0cce949db7bf

Transaction sent with hash: 0xca2a5a324b3c011f35a9e5e182c2f9e11b1714b739093b66b7bc0cce949db7bf

Example with an ERC-20 approval

const swap = await getSwap({
  fromAddress: account.address,
  from: degen,
  to: eth,
  amount: '10',
});

if (swap.approveTransaction) {
  // Approve DEGEN for spending
  const nonce = await publicClient.getTransactionCount({ address: account.address });
  const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas();
  const params: TransactionParams = { nonce, maxFeePerGas, maxPriorityFeePerGas };
  const tx = swap.approveTransaction.withParams(params);
  const serializedTransaction = await account.signTransaction(tx);
  const txHash = await publicClient.sendRawTransaction({ serializedTransaction });
  console.log(`Approval transaction sent with hash: ${txHash}`);
}

// Wait for approval
await delay(10000);

// Swap
const nonce = await publicClient.getTransactionCount({ address: account.address });
const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas();
const params: TransactionParams = { nonce, maxFeePerGas, maxPriorityFeePerGas };
const tx = swap.transaction.withParams(params);
const serializedTransaction = await account.signTransaction(tx);
const txHash = await publicClient.sendRawTransaction({ serializedTransaction });
console.log(`Transaction sent with hash: ${txHash}`);
}

Approval - https://basescan.org/tx/0x044bf9ed67b1c22d7d2c203222d9a6b66576d0058780d222b30fe4377aaf32d2
Swap - https://basescan.org/tx/0xe15755021e051a4fcfc02cfa083728022eb9262b74c5b271aca1dbf147bdd2aa

Approval transaction sent with hash: 0x044bf9ed67b1c22d7d2c203222d9a6b66576d0058780d222b30fe4377aaf32d2
Transaction sent with hash: 0xe15755021e051a4fcfc02cfa083728022eb9262b74c5b271aca1dbf147bdd2aa

@@ -7,6 +7,7 @@ module.exports = {
statements: 100,
},
},
maxWorkers: 1,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to enable serialization for bigint by jest

jestjs/jest#11617 (comment)

/**
* Note: exported as public Type
*/
export type TransactionParams = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is used to inject values from the wallet into the transaction

const swap = await getSwap({
  fromAddress: account.address,
  from: eth,
  to: degen,
  amount: '0.0001',
});

// Build the transaction
const nonce = await publicClient.getTransactionCount({ address: account.address });
const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas();

const params: TransactionParams = { nonce, maxFeePerGas, maxPriorityFeePerGas };

const tx = swap.transaction.withParams(params);

* Note: exported as public Type
*/
export interface Transaction {
transaction: TransactionData;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the object developers/users should pass into viem's signTransaction (https://viem.sh/docs/actions/wallet/signTransaction.html)

@0xAlec 0xAlec changed the title Alec/get trade feat: getSwap Jun 10, 2024
@0xAlec 0xAlec marked this pull request as ready for review June 10, 2024 13:56
@0xAlec 0xAlec requested review from Zizzamia and cpcramer June 10, 2024 13:56
Comment on lines 1 to +3
export const CDP_LISTSWAPASSETS = 'cdp_listSwapAssets';
export const CDP_GETSWAPQUOTE = 'cdp_getSwapQuote';
export const CDP_GETSWAPTRADE = 'cdp_getSwapTrade';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on updating these to be snake case?

Suggested change
export const CDP_LISTSWAPASSETS = 'cdp_listSwapAssets';
export const CDP_GETSWAPQUOTE = 'cdp_getSwapQuote';
export const CDP_GETSWAPTRADE = 'cdp_getSwapTrade';
export const CDP_LIST_SWAP_ASSETS = 'cdp_listSwapAssets';
export const CDP_GET_SWAP_QUOTE = 'cdp_getSwapQuote';
export const CDP_GET_SWAP_TRADE = 'cdp_getSwapTrade';

@@ -1,15 +1,17 @@
import { formatDecimals } from './formatDecimals';
import type { GetQuoteParams, GetQuoteAPIParams } from '../types';
import type { SwapParams, SwapAPIParams, GetSwapParams } from '../types';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem very similar to each other in terms of naming

Comment on lines +119 to +128
export type TransactionData = {
chainId: number; // The chain ID
data: `0x${string}`; // The data for the transaction
gas: bigint; // The gas limit
to: `0x${string}`; // The recipient address
value: bigint; // The value of the transaction
nonce?: number; // The nonce for the transaction
maxFeePerGas?: bigint | undefined; // The maximum fee per gas
maxPriorityFeePerGas?: bigint | undefined; // The maximum priority fee per gas
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we document what each type is in the comment above the function?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No next is fine, after all Types should feel easy to understand. Also a type works in pair on where it's used, so folks should be able to see where the Type is used.

@@ -1,10 +1,15 @@
// 🌲☀️🌲
export { getQuote } from './core/getQuote';
export { getSwap } from './core/getSwap';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xAlec more a an open question, is getSwap too short as name?

We wrote

Retrieves an unsigned transaction for a swap from Token A to Token B.

I wonder if get is the right verb, and maybe something like prepareSwapTransaction or buildSwapTransaction.

Let's not change the name yet, but would love to reflect a second on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like buildSwapTransaction!

warning: trade.quote.warning,
};
} catch (error) {
throw new Error(`getSwap: ${error}`);
Copy link
Contributor

@Zizzamia Zizzamia Jun 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something we should write on, is also an Error page in our docs, and overtime re-align all our code to it.

* Note: exported as public Type
*/
export type Swap = {
approveTransaction?: Transaction; // The approval transaction
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What a developer will do with the Approval Transaction? approve is a verb, and it feels more something to do that to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response will contain an approveTransaction (which needs to be broadcast before transaction) in order to approve e.g. Token A to be used by the contract in a swap between Token A to Token B. This is only applicable to ERC-20 tokens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
* Note: exported as public Type
*/
export type SwapParams = GetQuoteParams | GetSwapParams;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think joinging them will might confuse folks, because now fromAddress: Address; which is in Swap, it's not in Quote, but we allow it to have it now when use it in getQuote.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just keep the two type seprated?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok so you have getParamsForToken that depends on both, interesting.

const { from, to, amount, amountReference, isAmountInDecimals } = params;
const { fromAddress } = params as GetSwapParams;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add more comments here, I am a bit confused. Like who needs fromAddress and how they use it. And when is not there, will just be empty?

/**
* Constructs an unsigned transaction.
*/
export function getTransaction(tx: RawTransactionData, chainId: string): Transaction {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to change the name of this one, as I read this more get info from a particular transaction. Which I think in this case is slighly different.

Copy link
Contributor

@ilikesymmetry ilikesymmetry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asks:

  • add docs to the official docs pages
  • make it easier to locally test
  • start a new feature branch for swapkit so we can continue to push package updates without unfinished work

I'm default hesitant to be componentizing out of the gate without having a functional solution built, but I recognize it takes layers of iterations to get there. I would not be surprised if we fully rewrite the first 1-5 pieces of swapkit given how complex it is.

A lot of my comments are focused on typing, naming, and reusing existing libraries as much as possible to reduce our own code bloat.

export type GetQuoteParams = {
from: Token; // The source token for the swap
to: Token; // The destination token for the swap
amount: string; // The amount to be swapped
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more specific about naming conventions, is amount the amount in or out? Is it a min or is it fixed on either side of in/out? Can you elaborate on why we have this as a string versus a bigint?

from: Token; // The source token for the swap
to: Token; // The destination token for the swap
amount: string; // The amount to be swapped
amountReference?: string; // The reference amount for the swap
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what "reference amount for the swap" means, can you elaborate?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I get confused on this one always. Probably let's have a longer comment to explaining what's that about.

to: Token; // The destination token for the swap
amount: string; // The amount to be swapped
amountReference?: string; // The reference amount for the swap
isAmountInDecimals?: boolean; // Whether the amount is in decimals
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code will be easier to follow and debug if the types are stricter/consistent. More often than not, I think we should pass data in the native bigint type and apply decimals only for displaying formatted numbers.

Comment on lines +72 to +79
export type RawTransactionData = {
data: string; // The transaction data
from: string; // The sender address
gas: string; // The gas limit
gasPrice: string; // The gas price
to: string; // The recipient address
value: string; // The value of the transaction
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should be able to grab all types like this from viem? To build our intuition, if we're ever building custom utilities for core functionality (eg transaction types), then I think we should double check nothing from viem/wagmi can serve us.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ilikesymmetry if there are types we can use from Viem, can you just linking them. Some of them are easy to use, other might need some refactoring as they need to reflect more our API responses.

approveTransaction?: Transaction; // The approval transaction
fee: Fee; // The fee for the swap
quote: Quote; // The quote for the swap
transaction: Transaction; // The swap transaction
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit, but I think it's important for our job as a library)

Technically this is not a transaction, but a "call". This only becomes a transaction once it it signed and sent to the mempool, at which point we have a transaction hash to uniquely identify it. Following the recent work on 5792, I think we should call this swapCall and rename approveTransaction->approveCall

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh great point

* Note: exported as public Type
*/
export type Swap = {
approveTransaction?: Transaction; // The approval transaction
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm that the approve transaction uses Permit2? It seems like this calls are prepared by our API which I don't have insight into how it's estimating and preparing these things

Comment on lines +121 to +123
data: `0x${string}`; // The data for the transaction
gas: bigint; // The gas limit
to: `0x${string}`; // The recipient address
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0x${string} -> Address

/**
* Note: exported as public Type
*/
export type TransactionData = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here on being able to use a viem type

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which one are you thinking about?

@Zizzamia
Copy link
Contributor

I am going to merge this PR, and follow up with all the comments. So I can help @0xAlec with some other work he is doing. cc @ilikesymmetry @kyhyco @abcrane123

@Zizzamia Zizzamia merged commit dcd26b9 into main Jun 11, 2024
9 checks passed
@Zizzamia Zizzamia deleted the alec/getTrade branch June 11, 2024 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants