Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
kien-ngo committed Nov 27, 2024
1 parent 9d3c8ac commit dcd6b94
Show file tree
Hide file tree
Showing 7 changed files with 433 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/stupid-buses-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

Add 2 new Pay functions: convertFiatToCrypto and convertCryptoToFiat
10 changes: 10 additions & 0 deletions packages/thirdweb/src/exports/pay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,13 @@ export type {
PayTokenInfo,
PayOnChainTransactionDetails,
} from "../pay/utils/commonTypes.js";

export {
convertFiatToCrypto,
type ConvertFiatToCryptoParams,
} from "../pay/convert/fiatToCrypto.js";

export {
convertCryptoToFiat,
type ConvertCryptoToFiatParams,
} from "../pay/convert/cryptoToFiat.js";
95 changes: 95 additions & 0 deletions packages/thirdweb/src/pay/convert/cryptoToFiat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { base } from "../../chains/chain-definitions/base.js";
import { ethereum } from "../../chains/chain-definitions/ethereum.js";
import { sepolia } from "../../chains/chain-definitions/sepolia.js";
import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
import { convertCryptoToFiat } from "./cryptoToFiat.js";

describe.runIf(process.env.TW_SECRET_KEY)("Pay: crypto-to-fiat", () => {
it("should convert ETH price to USD on Ethereum mainnet", async () => {
const data = await convertCryptoToFiat({
chain: ethereum,
fromTokenAddress: NATIVE_TOKEN_ADDRESS,
fromAmount: 1,
to: "USD",
client: TEST_CLIENT,
});
expect(data.result).toBeDefined();
// Should be a number
expect(!Number.isNaN(data.result)).toBe(true);
// Since eth is around US$3000, we can add a test to check if the price is greater than $1500 (as a safe margin)
// let's hope that scenario does not happen :(
expect(Number(data.result) > 1500).toBe(true);
});

it("should convert ETH price to USD on Base mainnet", async () => {
const data = await convertCryptoToFiat({
chain: base,
fromTokenAddress: NATIVE_TOKEN_ADDRESS,
fromAmount: 1,
to: "USD",
client: TEST_CLIENT,
});
expect(data.result).toBeDefined();
// Should be a number
expect(!Number.isNaN(data.result)).toBe(true);
// Since eth is around US$3000, we can add a test to check if the price is greater than $1500 (as a safe margin)
// let's hope that scenario does not happen :(
expect(data.result > 1500).toBe(true);
});

it("should return zero if fromAmount is zero", async () => {
const data = await convertCryptoToFiat({
chain: base,
fromTokenAddress: NATIVE_TOKEN_ADDRESS,
fromAmount: 0,
to: "USD",
client: TEST_CLIENT,
});
expect(data.result).toBe(0);
});

it("should throw error for testnet chain (because testnets are not supported", async () => {
await expect(() =>
convertCryptoToFiat({
chain: sepolia,
fromTokenAddress: NATIVE_TOKEN_ADDRESS,
fromAmount: 1,
to: "USD",
client: TEST_CLIENT,
}),
).rejects.toThrowError(
`Cannot fetch price for a testnet (chainId: ${sepolia.id})`,
);
});

it("should throw error if fromTokenAddress is set to an invalid EVM address", async () => {
await expect(() =>
convertCryptoToFiat({
chain: ethereum,
fromTokenAddress: "haha",
fromAmount: 1,
to: "USD",
client: TEST_CLIENT,
}),
).rejects.toThrowError(
"Invalid fromTokenAddress. Expected a valid EVM contract address",
);
});

it("should throw error if fromTokenAddress is set to a wallet address", async () => {
await expect(() =>
convertCryptoToFiat({
chain: base,
fromTokenAddress: TEST_ACCOUNT_A.address,
fromAmount: 1,
to: "USD",
client: TEST_CLIENT,
}),
).rejects.toThrowError(
`Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`,
);
});
});
111 changes: 111 additions & 0 deletions packages/thirdweb/src/pay/convert/cryptoToFiat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Address } from "abitype";
import type { Chain } from "../../chains/types.js";
import type { ThirdwebClient } from "../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
import { getBytecode } from "../../contract/actions/get-bytecode.js";
import { getContract } from "../../contract/contract.js";
import { isAddress } from "../../utils/address.js";
import { getClientFetch } from "../../utils/fetch.js";
import { getPayConvertCryptoToFiatEndpoint } from "../utils/definitions.js";

/**
* Props for the `convertCryptoToFiat` function
* @buyCrypto
*/
export type ConvertCryptoToFiatParams = {
client: ThirdwebClient;
/**
* The contract address of the token
* For native token, use NATIVE_TOKEN_ADDRESS
*/
fromTokenAddress: Address;
/**
* The amount of token to convert to fiat value
*/
fromAmount: number;
/**
* The chain that the token is deployed to
*/
chain: Chain;
/**
* The fiat symbol. e.g "USD"
* Only USD is supported at the moment.
*/
to: "USD";
};

/**
* Get a price of a token (using tokenAddress + chainId) in fiat.
* Only USD is supported at the moment.
* @example
* ### Basic usage
* For native token (non-ERC20), you should use NATIVE_TOKEN_ADDRESS as the value for `tokenAddress`
* ```ts
* import { convertCryptoToFiat } from "thirdweb/pay";
*
* // Get Ethereum price
* const result = convertCryptoToFiat({
* fromTokenAddress: NATIVE_TOKEN_ADDRESS,
* to: "USD",
* chain: ethereum,
* fromAmount: 1,
* });
*
* // Result: 3404.11
* ```
* @buyCrypto
* @returns a number representing the price (in selected fiat) of "x" token, with "x" being the `fromAmount`.
*/
export async function convertCryptoToFiat(
options: ConvertCryptoToFiatParams,
): Promise<{ result: number }> {
const { client, fromTokenAddress, to, chain, fromAmount } = options;
if (Number(fromAmount) === 0) {
return { result: 0 };
}
// Testnets just don't work with our current provider(s)
if (chain.testnet === true) {
throw new Error(`Cannot fetch price for a testnet (chainId: ${chain.id})`);
}
// Some provider that we are using will return `0` for unsupported token
// so we should do some basic input validations before sending the request

// Make sure it's a valid EVM address
if (!isAddress(fromTokenAddress)) {
throw new Error(
"Invalid fromTokenAddress. Expected a valid EVM contract address",
);
}
// Make sure it's either a valid contract or a native token address
if (fromTokenAddress.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase()) {
const bytecode = await getBytecode(
getContract({
address: fromTokenAddress,
chain,
client,
}),
).catch(() => undefined);
if (!bytecode || bytecode === "0x") {
throw new Error(
`Error: ${fromTokenAddress} on chainId: ${chain.id} is not a valid contract address.`,
);
}
}
const params = {
fromTokenAddress,
to,
chainId: String(chain.id),
fromAmount: String(fromAmount),
};
const queryString = new URLSearchParams(params).toString();
const url = `${getPayConvertCryptoToFiatEndpoint()}?${queryString}`;
const response = await getClientFetch(client)(url);
if (!response.ok) {
throw new Error(
`Failed to fetch ${to} value for token (${fromTokenAddress}) on chainId: ${chain.id}`,
);
}

const data: { result: number } = await response.json();
return data;
}
96 changes: 96 additions & 0 deletions packages/thirdweb/src/pay/convert/fiatToCrypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { base } from "../../chains/chain-definitions/base.js";
import { ethereum } from "../../chains/chain-definitions/ethereum.js";
import { sepolia } from "../../chains/chain-definitions/sepolia.js";
import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
import { convertFiatToCrypto } from "./fiatToCrypto.js";

describe.runIf(process.env.TW_SECRET_KEY)("Pay: fiatToCrypto", () => {
it("should convert fiat price to token on Ethereum mainnet", async () => {
const data = await convertFiatToCrypto({
chain: ethereum,
from: "USD",
fromAmount: 1,
to: NATIVE_TOKEN_ADDRESS,
client: TEST_CLIENT,
});
expect(data.result).toBeDefined();
// Should be a number
expect(!Number.isNaN(data.result)).toBe(true);
// Since eth is around US$3000, 1 USD should be around 0.0003
// we give it some safe margin so the test won't be flaky
expect(data.result < 0.001).toBe(true);
});

it("should convert fiat price to token on Base mainnet", async () => {
const data = await convertFiatToCrypto({
chain: base,
from: "USD",
fromAmount: 1,
to: NATIVE_TOKEN_ADDRESS,
client: TEST_CLIENT,
});

expect(data.result).toBeDefined();
// Should be a number
expect(!Number.isNaN(data.result)).toBe(true);
// Since eth is around US$3000, 1 USD should be around 0.0003
// we give it some safe margin so the test won't be flaky
expect(data.result < 0.001).toBe(true);
});

it("should return zero if the fromAmount is zero", async () => {
const data = await convertFiatToCrypto({
chain: base,
from: "USD",
fromAmount: 0,
to: NATIVE_TOKEN_ADDRESS,
client: TEST_CLIENT,
});
expect(data.result).toBe(0);
});

it("should throw error for testnet chain (because testnets are not supported", async () => {
await expect(() =>
convertFiatToCrypto({
chain: sepolia,
to: NATIVE_TOKEN_ADDRESS,
fromAmount: 1,
from: "USD",
client: TEST_CLIENT,
}),
).rejects.toThrowError(
`Cannot fetch price for a testnet (chainId: ${sepolia.id})`,
);
});

it("should throw error if `to` is set to an invalid EVM address", async () => {
await expect(() =>
convertFiatToCrypto({
chain: ethereum,
to: "haha",
fromAmount: 1,
from: "USD",
client: TEST_CLIENT,
}),
).rejects.toThrowError(
"Invalid `to`. Expected a valid EVM contract address",
);
});

it("should throw error if `to` is set to a wallet address", async () => {
await expect(() =>
convertFiatToCrypto({
chain: base,
to: TEST_ACCOUNT_A.address,
fromAmount: 1,
from: "USD",
client: TEST_CLIENT,
}),
).rejects.toThrowError(
`Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`,
);
});
});
Loading

0 comments on commit dcd6b94

Please sign in to comment.