diff --git a/src/request.ts b/src/request.ts index 97bb05f..7398566 100644 --- a/src/request.ts +++ b/src/request.ts @@ -2,38 +2,9 @@ import { ethers, getDefaultProvider } from 'ethersv5' import { EPeanutLinkType, IPeanutUnsignedTransaction } from './consts/interfaces.consts' import { ERC20_ABI, LATEST_STABLE_BATCHER_VERSION } from './data' import { config, interfaces, prepareApproveERC20Tx, resolveFromEnsName } from '.' -import { normalizePath, routeForTargetAmount } from './util' +import { routeForTargetAmount } from './util' // INTERFACES -export interface ICreateRequestLinkProps { - chainId: string - tokenAddress: string - tokenAmount: string - tokenType: EPeanutLinkType - tokenDecimals: string - recipientAddress: string - tokenSymbol?: string - recipientName?: string - baseUrl?: string - apiUrl?: string - trackId?: string - reference?: string - attachment?: File - APIKey?: string -} - -export type IGetRequestLinkDetailsProps = { - APIKey?: string - apiUrl?: string -} & ( - | { - link: string - } - | { - uuid: string - } -) - export interface IPrepareRequestLinkFulfillmentTransactionProps { recipientAddress: string tokenAddress: string @@ -51,138 +22,20 @@ export type IPrepareXchainRequestFulfillmentTransactionProps = { provider: ethers.providers.Provider tokenType: EPeanutLinkType slippagePercentage?: number -} & ( - | { - link: string - apiUrl?: string - APIKey?: string - } - | { - linkDetails: Pick< - IGetRequestLinkDetailsResponse, - 'chainId' | 'recipientAddress' | 'tokenAmount' | 'tokenDecimals' | 'tokenAddress' - > - } -) - -export interface ISubmitRequestLinkFulfillmentProps { - hash: string - chainId: string - payerAddress: string - signedTx?: string - apiUrl?: string - link: string - amountUsd: string -} - -export interface ICreateRequestLinkResponse { - link: string -} - -export interface IGetRequestLinkDetailsResponse { - uuid: string - link: string - chainId: string - recipientAddress: string | null - tokenAmount: string - tokenAddress: string - tokenDecimals: number - tokenType: string - tokenSymbol: string | null - createdAt: string - updatedAt: string - reference: string | null - attachmentUrl: string | null - payerAddress: string | null - trackId: string | null - destinationChainFulfillmentHash: string | null - originChainFulfillmentHash: string | null - status: string + linkDetails: { + chainId: string + recipientAddress: string | null + tokenAmount: string + tokenAddress: string + tokenDecimals: number + } } export interface IPrepareRequestLinkFulfillmentTransactionResponse { unsignedTx: IPeanutUnsignedTransaction } -export interface ISubmitRequestLinkFulfillmentResponse { - success: boolean -} - // FUNCTIONS -export async function createRequestLink({ - chainId, - tokenAddress, - tokenAmount, - tokenType, - tokenDecimals, - recipientAddress, - baseUrl = 'https://peanut.to/request/pay', - apiUrl = 'https://api.peanut.to/', - trackId, - reference, - attachment, - recipientName, - tokenSymbol, - APIKey, -}: ICreateRequestLinkProps): Promise { - try { - const formData = new FormData() - formData.append('chainId', chainId) - formData.append('tokenAddress', tokenAddress) - formData.append('tokenAmount', tokenAmount) - formData.append('tokenType', tokenType.toString()) - formData.append('tokenDecimals', tokenDecimals) - formData.append('recipientAddress', recipientAddress) - formData.append('baseUrl', baseUrl) - - if (trackId) formData.append('trackId', trackId) - if (reference) formData.append('reference', reference) - if (recipientName) formData.append('recipientName', recipientName) - if (tokenSymbol) formData.append('tokenSymbol', tokenSymbol) - if (attachment) formData.append('attachment', attachment) - - const apiResponse = await fetch(normalizePath(`${apiUrl}/request-links`), { - method: 'POST', - body: formData, - headers: { - 'api-key': APIKey!, - }, - }) - - if (apiResponse.status !== 200) { - throw new Error('Failed to create request link') - } - - const { link } = await apiResponse.json() - return { link } - } catch (error) { - console.error('Error creating request link:', error) - throw error - } -} - -export async function getRequestLinkDetails( - props: IGetRequestLinkDetailsProps -): Promise { - const { APIKey, apiUrl = 'https://api.peanut.to/' } = props - - const uuid = 'uuid' in props ? props.uuid : getUuidFromLink(props.link) - - const apiResponse = await fetch(normalizePath(`${apiUrl}/request-links/${uuid}`), { - method: 'GET', - headers: { - 'api-key': APIKey!, - }, - }) - - if (apiResponse.status !== 200) { - throw new Error('Failed to get request link details') - } - - const responseData = await apiResponse.json() - - return responseData -} export async function prepareXchainRequestFulfillmentTransaction( props: IPrepareXchainRequestFulfillmentTransactionProps @@ -196,17 +49,8 @@ export async function prepareXchainRequestFulfillmentTransaction( provider, tokenType, slippagePercentage, + linkDetails, } = props - let linkDetails: Pick< - IGetRequestLinkDetailsResponse, - 'chainId' | 'recipientAddress' | 'tokenAmount' | 'tokenDecimals' | 'tokenAddress' - > - if ('linkDetails' in props) { - linkDetails = props.linkDetails - } else { - const { link, apiUrl = 'https://api.peanut.to/', APIKey } = props - linkDetails = await getRequestLinkDetails({ link, apiUrl, APIKey }) - } let { chainId: destinationChainId, recipientAddress, @@ -374,45 +218,24 @@ export function prepareRequestLinkFulfillmentTransaction({ } } -export async function submitRequestLinkFulfillment({ - chainId, - hash, - payerAddress, - apiUrl = 'https://api.peanut.to/', - link, - amountUsd, -}: ISubmitRequestLinkFulfillmentProps): Promise { - try { - const uuid = getUuidFromLink(link) - const apiResponse = await fetch(normalizePath(`${apiUrl}/request-links/${uuid}`), { - method: 'PATCH', - body: JSON.stringify({ - destinationChainFulfillmentHash: hash, - payerAddress, - chainId, - amountUsd, - // signedTx, - }), - headers: { - 'Content-Type': 'application/json', - }, - }) +function throwDeprecatedError(): never { + throw new Error( + 'This function has been deprecated. Please check how to use the request API in https://docs.peanut.to/request-api' + ) +} - if (apiResponse.status !== 200) { - throw new Error('Failed to submit request link fulfillment') - } +export function createRequestLink(_data: object): Promise { + throwDeprecatedError() +} - const data = await apiResponse.json() +export function getRequestLinkDetails(_data: object): Promise { + throwDeprecatedError() +} - return data - } catch (error) { - console.error('Error submitting request link fulfillment:', error) - throw error - } // TODO: handle error +export function submitRequestLinkFulfillment(_data: object): Promise { + throwDeprecatedError() } -export function getUuidFromLink(link: string): string { - const url = new URL(link) - const searchParams = new URLSearchParams(url.search) - return searchParams.get('id')! +export function getUuidFromLink(_data: string): string { + throwDeprecatedError() } diff --git a/test/basic/RequestLinkXChain.test.ts b/test/basic/RequestLinkXChain.test.ts deleted file mode 100644 index 4b2c35f..0000000 --- a/test/basic/RequestLinkXChain.test.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { ethers } from 'ethersv5' -import peanut, { getDefaultProvider, getSquidRouterUrl, signAndSubmitTx } from '../../src/index' // import directly from source code -import dotenv from 'dotenv' -import { describe, it } from '@jest/globals' -import { BigNumber } from 'ethersv5' -import { EPeanutLinkType } from '../../src/consts/interfaces.consts' -dotenv.config() - -const retry = async (assertion: () => Promise, { times = 3, interval = 100 } = {}) => { - for (let i = 0; i < times; i++) { - try { - await assertion() - return - } catch (err) { - if (i === times - 1) throw err - await new Promise((resolve) => setTimeout(resolve, interval)) - } - } -} - -describe('Peanut XChain request links fulfillment tests', function () { - it.each([ - { - amount: '0.1', - sourceToken: { - chain: '10', - address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on Optimism - decimals: 6, - name: 'USDC on Optimism', - type: EPeanutLinkType.erc20, - }, - destinationToken: { - chain: '137', - address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', // USDT on Polygon - decimals: 6, - name: 'USDT on Polygon', - type: EPeanutLinkType.erc20, - }, - }, - { - amount: '0.0001', - sourceToken: { - chain: '10', - address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // ETH on Optimism - decimals: 18, - name: 'ETH on Optimism', - type: EPeanutLinkType.native, - }, - destinationToken: { - chain: '42161', - address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // ETH on Arbitrum - decimals: 18, - name: 'ETH on Arbitrum', - type: EPeanutLinkType.native, - }, - }, - { - amount: '0.1', - sourceToken: { - chain: '10', - address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // ETH on Optimism - decimals: 18, - name: 'ETH on Optimism', - type: EPeanutLinkType.native, - }, - destinationToken: { - chain: '10', - address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on Optimism - decimals: 6, - name: 'USDC on Optimism', - type: EPeanutLinkType.erc20, - }, - }, - ])( - '$sourceToken.name to $destinationToken.name', - async ({ amount, sourceToken, destinationToken }) => { - peanut.toggleVerbose(true) - const userPrivateKey = process.env.TEST_WALLET_PRIVATE_KEY! - - // Parameters that affect the test behaviour - const apiUrl = process.env.PEANUT_API_URL! - const APIKey = process.env.PEANUT_DEV_API_KEY! - const sourceChainProvider = await getDefaultProvider(sourceToken.chain) - console.log('Source chain provider', sourceChainProvider) - - const userSourceChainWallet = new ethers.Wallet(userPrivateKey, sourceChainProvider) - - const recipientAddress = new ethers.Wallet(process.env.TEST_WALLET_PRIVATE_KEY2!).address - const initialBalance = await peanut.getTokenBalance({ - tokenAddress: destinationToken.address, - walletAddress: recipientAddress, - chainId: destinationToken.chain, - }) - console.log('Initial balance of recipient:', initialBalance) - - const { link } = await peanut.createRequestLink({ - chainId: destinationToken.chain, - tokenAddress: destinationToken.address, - tokenAmount: amount, - tokenType: destinationToken.type, - tokenDecimals: destinationToken.decimals.toString(), - recipientAddress, - APIKey, - apiUrl, - }) - console.log('Created a request link on the source chain!', link) - - const linkDetails = await peanut.getRequestLinkDetails({ - link, - APIKey, - apiUrl, - }) - console.log('Got the link details!', linkDetails) - - const xchainUnsignedTxs = await peanut.prepareXchainRequestFulfillmentTransaction({ - fromChainId: sourceToken.chain, - senderAddress: userSourceChainWallet.address, - fromToken: sourceToken.address, - squidRouterUrl: getSquidRouterUrl(true, false), - provider: sourceChainProvider, - tokenType: sourceToken.type, - fromTokenDecimals: sourceToken.decimals, - linkDetails, - }) - console.log('Computed x chain unsigned fulfillment transactions', xchainUnsignedTxs) - - for (const unsignedTx of xchainUnsignedTxs.unsignedTxs) { - const { tx, txHash } = await signAndSubmitTx({ - unsignedTx, - structSigner: { - signer: userSourceChainWallet, - gasLimit: BigNumber.from(2_000_000), - }, - }) - - console.log('Submitted a transaction to fulfill the request link with tx hash', txHash) - await tx.wait() - console.log('Request link fulfillment initiated!') - } - // how many digits to check for equality after the decimal point - const numDigits = Math.floor(Math.log10(1 / Number(amount))) + 1 - const expectedBalance = Number(initialBalance) + Number(amount) - - await retry( - async () => { - const finalBalance = await peanut.getTokenBalance({ - tokenAddress: destinationToken.address, - walletAddress: recipientAddress, - chainId: destinationToken.chain, - }) - console.log( - `Final balance of recipient: ${finalBalance}, expected: ${expectedBalance}, with tolerance: ${numDigits}` - ) - expect(Number(finalBalance)).toBeCloseTo(expectedBalance, numDigits) - }, - { times: 15, interval: 2000 } - ) // retry for up to 30 seconds - }, - 120000 - ) - - it('should fetch link details when not provided', async () => { - const amount = '0.1' - const sourceToken = { - chain: '10', - address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on Optimism - decimals: 6, - name: 'USDC on Optimism', - type: EPeanutLinkType.erc20, - } - const destinationToken = { - chain: '137', - address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon - decimals: 6, - name: 'DAI on Polygon', - type: EPeanutLinkType.erc20, - } - peanut.toggleVerbose(true) - const userPrivateKey = process.env.TEST_WALLET_PRIVATE_KEY! - - // Parameters that affect the test behaviour - const apiUrl = process.env.PEANUT_API_URL! - const APIKey = process.env.PEANUT_DEV_API_KEY! - const sourceChainProvider = await getDefaultProvider(sourceToken.chain) - console.log('Source chain provider', sourceChainProvider) - - const userSourceChainWallet = new ethers.Wallet(userPrivateKey, sourceChainProvider) - - const recipientAddress = new ethers.Wallet(process.env.TEST_WALLET_PRIVATE_KEY2!).address - const initialBalance = await peanut.getTokenBalance({ - tokenAddress: destinationToken.address, - walletAddress: recipientAddress, - chainId: destinationToken.chain, - }) - console.log('Initial balance of recipient:', initialBalance) - - const { link } = await peanut.createRequestLink({ - chainId: destinationToken.chain, - tokenAddress: destinationToken.address, - tokenAmount: amount, - tokenType: destinationToken.type, - tokenDecimals: destinationToken.decimals.toString(), - recipientAddress, - APIKey, - apiUrl, - }) - console.log('Created a request link on the source chain!', link) - - const xchainUnsignedTxs = await peanut.prepareXchainRequestFulfillmentTransaction({ - fromChainId: sourceToken.chain, - senderAddress: userSourceChainWallet.address, - fromToken: sourceToken.address, - squidRouterUrl: getSquidRouterUrl(true, false), - provider: sourceChainProvider, - tokenType: sourceToken.type, - fromTokenDecimals: sourceToken.decimals, - link, - APIKey, - apiUrl, - }) - console.log('Computed x chain unsigned fulfillment transactions', xchainUnsignedTxs) - - for (const unsignedTx of xchainUnsignedTxs.unsignedTxs) { - const { tx, txHash } = await signAndSubmitTx({ - unsignedTx, - structSigner: { - signer: userSourceChainWallet, - gasLimit: BigNumber.from(2_000_000), - }, - }) - - console.log('Submitted a transaction to fulfill the request link with tx hash', txHash) - await tx.wait() - console.log('Request link fulfillment initiated!') - } - // how many digits to check for equality after the decimal point - const numDigits = Math.floor(Math.log10(1 / Number(amount))) + 1 - const expectedBalance = Number(initialBalance) + Number(amount) - - await retry( - async () => { - const finalBalance = await peanut.getTokenBalance({ - tokenAddress: destinationToken.address, - walletAddress: recipientAddress, - chainId: destinationToken.chain, - }) - console.log( - `Final balance of recipient: ${finalBalance}, expected: ${expectedBalance}, with tolerance: ${numDigits}` - ) - expect(Number(finalBalance)).toBeCloseTo(expectedBalance, numDigits) - }, - { times: 15, interval: 2000 } - ) // retry for up to 30 seconds - }, 120000) -}) diff --git a/test/unit/request.test.ts b/test/unit/request.test.ts deleted file mode 100644 index f77950b..0000000 --- a/test/unit/request.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { createRequestLink, getRequestLinkDetails, submitRequestLinkFulfillment } from '../../src/request' - -const mockFetch = jest.fn() - -beforeEach(() => { - jest.spyOn(global, 'fetch').mockImplementation(mockFetch) -}) - -afterEach(() => { - jest.restoreAllMocks() - jest.clearAllMocks() -}) - -describe('Request', () => { - describe('createRequestLink', () => { - it('should complete apiUrl if not provided', async () => { - mockFetch.mockResolvedValueOnce({ - json: jest.fn().mockResolvedValueOnce({ - link: 'https://peanut.to/request/pay?id=123456789', - }), - status: 200, - }) - - await createRequestLink({ - chainId: '137', - tokenAddress: '0x0000000000000000000000000000000000000000', - tokenAmount: '0.1', - tokenType: 0, - tokenDecimals: '18', - recipientAddress: '0x0000000000000000000000000000000000000000', - }) - - expect(mockFetch).toHaveBeenCalledWith('https://api.peanut.to/request-links', expect.anything()) - }) - }) - - describe('getRequestLinkDetails', () => { - it('should complete apiUrl if not provided', async () => { - const linkUuid = 'ee3b904d-9809-4b97-b82b-34de1d1a7314' - mockFetch.mockResolvedValueOnce({ - json: jest.fn().mockResolvedValueOnce({ - uuid: linkUuid, - link: `https://peanut.to/request/pay?id=${linkUuid}`, - chainId: '137', - recipientAddress: '0x0000000000000000000000000000000000000000', - tokenAmount: '0.1', - tokenAddress: '0x0000000000000000000000000000000000000000', - tokenDecimals: 18, - tokenType: 0, - createdAt: '2023-05-24T15:00:00.000Z', - updatedAt: '2023-05-24T15:00:00.000Z', - status: 'pending', - }), - status: 200, - }) - - await getRequestLinkDetails({ - link: `https://peanut.to/request/pay?id=${linkUuid}`, - }) - - expect(mockFetch).toHaveBeenCalledWith(`https://api.peanut.to/request-links/${linkUuid}`, expect.anything()) - }) - - it('should accept uuid directly', async () => { - const linkUuid = 'ee3b904d-9809-4b97-b82b-34de1d1a7314' - mockFetch.mockResolvedValueOnce({ - json: jest.fn().mockResolvedValueOnce({ - link: `https://peanut.to/request/pay?id=${linkUuid}`, - }), - status: 200, - }) - - await getRequestLinkDetails({ uuid: linkUuid }) - - expect(mockFetch).toHaveBeenCalledWith(`https://api.peanut.to/request-links/${linkUuid}`, expect.anything()) - }) - }) - - describe('submitRequestLinkFulfillment', () => { - it('should complete apiUrl if not provided', async () => { - const linkUuid = 'ee3b904d-9809-4b97-b82b-34de1d1a7314' - mockFetch.mockResolvedValueOnce({ - json: jest.fn().mockResolvedValueOnce({ - success: true, - }), - status: 200, - }) - - await submitRequestLinkFulfillment({ - chainId: '137', - hash: '0x0000000000000000000000000000000000000000', - payerAddress: '0x0000000000000000000000000000000000000000', - signedTx: '0x0000000000000000000000000000000000000000', - link: `https://peanut.to/request/pay?id=${linkUuid}`, - amountUsd: '0.1', - }) - - expect(mockFetch).toHaveBeenCalledWith(`https://api.peanut.to/request-links/${linkUuid}`, expect.anything()) - }) - }) -})