-
Notifications
You must be signed in to change notification settings - Fork 387
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
435 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"thirdweb": minor | ||
--- | ||
|
||
Add 2 new Pay functions: convertFiatToCrypto and convertCryptoToFiat |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
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, | ||
* // This is not case sensitive, so either "USD" or "usd" is fine | ||
* 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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`, | ||
); | ||
}); | ||
}); |
Oops, something went wrong.