-
Notifications
You must be signed in to change notification settings - Fork 4
feat: SDK POC #11
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: SDK POC #11
Changes from all commits
4dd6552
c2446fe
30decbf
2f109d0
a0039bb
f3223e1
6a12e32
16264bd
c3babfa
0f514ee
f18dae0
1725b1f
2be2690
be72cba
933a9ec
03b73f6
b735448
a8a403f
7afab47
044738f
4548a76
76f34d0
1ec53d2
833c57e
4d729d3
ff89420
5945e2a
c1f3d65
dc145a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # Gopher SDK | ||
|
|
||
| Gopher SDK is a powerful library for interacting with blockchain networks, allowing you to retrieve information about fungible tokens, supported chains, and user balances, as well as finding solutions for asset transfers. | ||
|
|
||
| ## Installation | ||
|
|
||
| Install the library using npm or yarn: | ||
|
|
||
| ```bash | ||
| npm install gopher-sdk | ||
| # or | ||
| yarn add gopher-sdk | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| You have two approaches: using the `Gopher` class or calling API endpoints directly through the SDK. | ||
|
|
||
| ### Using the Gopher Class | ||
|
|
||
| The `Gopher` class provides a convenient interface for interacting with the blockchain using an EIP1193 provider (e.g., MetaMask). | ||
|
|
||
| #### Example | ||
|
|
||
| ```typescript | ||
| import { Gopher } from 'gopher-sdk'; | ||
|
|
||
| const gopher = new Gopher(window.ethereum); | ||
|
|
||
| gopher.getUserBalances().then(console.log); | ||
| ``` | ||
|
|
||
| ### Calling API Endpoints Directly | ||
|
|
||
| Alternatively, you can call the API endpoints directly using the provided SDK functions. | ||
|
|
||
| #### Example | ||
|
|
||
| ```typescript | ||
| import { api } from 'gopher-sdk'; | ||
|
|
||
| const ownerAddress = "0x3E101Ec02e7A48D16DADE204C96bFF842E7E2519"; | ||
| const tokenSymbol = "USDC"; | ||
|
|
||
| api.getUserFungibleTokens(ownerAddress, tokenSymbol).then(console.log); | ||
| ``` | ||
|
|
||
| ### Environment Variables | ||
|
|
||
| The SDK uses environment variables to configure the base URL `GOPHER_URL`. You can set this variable in your environment configuration or directly in your code. | ||
|
|
||
| #### Setting Environment Variables in Code | ||
|
|
||
| ```typescript | ||
| import { setBaseUrl } from 'gopher-sdk'; | ||
|
|
||
| setBaseUrl("http://localhost:8080"); | ||
| ``` | ||
|
|
||
| ## Contributing | ||
|
|
||
| Contributions are welcome! Please submit a pull request or open an issue to discuss any changes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { | ||
| Address, | ||
| Chain, | ||
| ChainID, | ||
| FailedSolution, | ||
| FungibleToken, | ||
| FungibleTokenBalance, | ||
| Solution, | ||
| SolutionOptions, | ||
| SolutionResponse, | ||
| TokenSymbol, | ||
| } from "./types"; | ||
| import { getEnv } from "./utils"; | ||
|
|
||
| export let BASE_URL = getEnv( | ||
| "GOPHER_URL", | ||
| "https://gopher.test.buildwithsygma.com/" | ||
| ); | ||
| export function setBaseUrl(url: string): void { | ||
| BASE_URL = url; | ||
| } | ||
|
|
||
| export async function getSupportedChains(): Promise<Chain[]> { | ||
| const url = new URL("/networks", BASE_URL); | ||
| const response = await fetch(url).then( | ||
| (response) => response.json() as unknown as { data: Chain[] } | ||
| ); | ||
|
|
||
| return response.data; | ||
| } | ||
|
|
||
| export async function getChainTokens( | ||
| chainID: ChainID | ||
| ): Promise<FungibleToken[]> { | ||
| const url = new URL(`/networks/${chainID}/assets/fungible`, BASE_URL); | ||
| const response = await fetch(url).then( | ||
| (response) => response.json() as unknown as { data: FungibleToken[] } | ||
| ); | ||
|
|
||
| return response.data; | ||
| } | ||
|
|
||
| export async function getFungibleTokens(): Promise<FungibleToken[]> { | ||
| const url = new URL("/assets/fungible", BASE_URL); | ||
| const response = await fetch(url).then( | ||
| (response) => response.json() as unknown as { data: FungibleToken[] } | ||
| ); | ||
|
|
||
| return response.data; | ||
| } | ||
|
|
||
| export async function getFungibleToken( | ||
| token: TokenSymbol | ||
| ): Promise<FungibleToken> { | ||
| const url = new URL(`/assets/fungible/${token}`, BASE_URL); | ||
| return await fetch(url).then( | ||
| (response) => response.json() as unknown as FungibleToken | ||
| ); | ||
| } | ||
|
|
||
| export async function getUserFungibleTokens( | ||
| address: Address, | ||
| token: TokenSymbol | ||
| ): Promise<FungibleTokenBalance[]> { | ||
| const url = new URL( | ||
| `/accounts/${address}/assets/fungible/${token}`, | ||
| BASE_URL | ||
| ); | ||
| const response = await fetch(url).then( | ||
| (response) => response.json() as unknown as { data: FungibleTokenBalance[] } | ||
| ); | ||
|
|
||
| return response.data; | ||
| } | ||
|
|
||
| export async function getSolution({ | ||
| account, | ||
| destinationChain, | ||
| token, | ||
| amount, | ||
| threshold, | ||
| whitelistedSourceChains, | ||
| }: SolutionOptions): Promise<SolutionResponse> { | ||
| const url = new URL("/solutions/aggregation", BASE_URL); | ||
|
|
||
| url.searchParams.set("account", account); | ||
| url.searchParams.set("destination", String(destinationChain)); | ||
| url.searchParams.set("token", token); | ||
| url.searchParams.set("amount", String(amount)); | ||
| // | ||
| if (threshold) url.searchParams.set("threshold", String(threshold)); | ||
| if (whitelistedSourceChains?.length) | ||
| url.searchParams.set( | ||
| "whitelistedSourceChains", | ||
| whitelistedSourceChains.join(",") | ||
| ); | ||
|
|
||
| const response = await fetch(url).then( | ||
| (response) => | ||
| response.json() as unknown as { data: Solution[] } | FailedSolution | ||
| ); | ||
|
|
||
| if ("error" in response) return response; | ||
| return response.data; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export enum ChainType { | ||
| EVM = "evm", | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,93 @@ | ||
| import { Chain, FungibleToken, Solution, SolutionOptions } from "./types"; | ||
| import { EIP1193Provider } from "eip1193-types"; | ||
| import { | ||
| getFungibleTokens, | ||
| getSolution, | ||
| getSupportedChains, | ||
| getUserFungibleTokens, | ||
| setBaseUrl, | ||
| BASE_URL, | ||
| } from "./api"; | ||
| import { | ||
| Address, | ||
| Chain, | ||
| FungibleToken, | ||
| FungibleTokenBalance, | ||
| SolutionOptions, | ||
| SolutionResponse, | ||
| TokenSymbol, | ||
| } from "./types"; | ||
|
|
||
| export async function getSupportedChains(): Promise<Chain[]> { | ||
| return Promise.resolve([]); | ||
| } | ||
| export type * from "./types"; | ||
| export * as api from "./api"; | ||
| export * from "./enums"; | ||
|
|
||
| export async function getKnownFungibleTokens(): Promise<FungibleToken[]> { | ||
| return Promise.resolve([]); | ||
| } | ||
| class Gopher { | ||
| #provider: EIP1193Provider; | ||
|
|
||
| // local "cache" | ||
| #tokens?: FungibleToken[]; | ||
| #chains?: Chain[]; | ||
|
|
||
| constructor(provider: EIP1193Provider) { | ||
| this.#provider = provider; | ||
| } | ||
|
|
||
| public async getAvailableTokens(): Promise<FungibleToken[]> { | ||
| if (!this.#tokens) this.#tokens = await getFungibleTokens(); | ||
| return this.#tokens; | ||
| } | ||
|
|
||
| public async getAvailableChains(): Promise<Chain[]> { | ||
| if (!this.#chains) this.#chains = await getSupportedChains(); | ||
| return this.#chains; | ||
| } | ||
|
|
||
| public async getUserBalances(tokens?: FungibleToken[]): Promise<{ | ||
| [sybol: TokenSymbol]: { balances: FungibleTokenBalance[]; total: string }; | ||
| }> { | ||
| const account = await this.getAccount(); | ||
|
|
||
| export async function getSolution({ | ||
| account, | ||
| destinationChain, | ||
| token, | ||
| amount, | ||
| threshold, | ||
| whitelistedSourceChains, | ||
| }: SolutionOptions): Promise<Solution[]> { | ||
| console.log( | ||
| account, | ||
| destinationChain, | ||
| token, | ||
| amount, | ||
| threshold, | ||
| whitelistedSourceChains | ||
| ); | ||
| return Promise.resolve([]); | ||
| const tokenList = tokens || (await this.getAvailableTokens()); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This tricky question is like if you set an intentionally empty array (it is not logical to do) as an optional parameter and you got the result that is not part that reflects that array. |
||
|
|
||
| const balances = await Promise.all( | ||
| tokenList.map((token) => | ||
| getUserFungibleTokens(account, token.symbol).then((balances) => ({ | ||
| symbol: token.symbol, | ||
| balances, | ||
| })) | ||
| ) | ||
| ); | ||
|
|
||
| return balances.reduce((previousValue, { symbol, balances }) => { | ||
| previousValue[symbol] = { | ||
| total: balances | ||
| .reduce((prev, cur) => prev + BigInt(cur.balance), 0n) | ||
| .toString(), | ||
| balances, | ||
| }; | ||
| return previousValue; | ||
| }, {} as { [symbol: TokenSymbol]: { balances: FungibleTokenBalance[]; total: string } }); | ||
| } | ||
|
|
||
| public async getSolution( | ||
| settings: Omit<SolutionOptions, "account">, | ||
| targetAccount?: Address | ||
| ): Promise<SolutionResponse> { | ||
| const account = targetAccount || (await this.getAccount()); | ||
|
|
||
| return await getSolution({ ...settings, account }); | ||
| } | ||
|
|
||
| private async getAccount(): Promise<Address> { | ||
| const [account] = (await this.#provider.request({ | ||
| method: "eth_requestAccounts", | ||
| params: [], | ||
| })) as Address[]; | ||
| if (!account) | ||
| throw new Error("No available account! Check your provider or something"); | ||
|
|
||
| return account; | ||
| } | ||
| } | ||
|
|
||
| export { Gopher, setBaseUrl, BASE_URL, EIP1193Provider }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,76 @@ | ||
| export interface Chain {} | ||
| export interface FungibleToken {} | ||
| import { ChainType } from "./enums"; | ||
|
|
||
| export interface Solution {} | ||
| export type Address = `0x${string}`; | ||
|
|
||
| export type TokenSymbol = string; | ||
|
|
||
| export type ChainID = number; | ||
|
|
||
| export interface FungibleToken { | ||
| addresses: Record<ChainID, Address>; | ||
| decimals: number; | ||
| logoURI: string; | ||
| name: string; | ||
| symbol: TokenSymbol; | ||
| } | ||
|
|
||
| export interface Chain { | ||
| chainID: ChainID; | ||
| chainType: ChainType; | ||
| name: string; | ||
| logoURI: string; | ||
| rpcurls: string[]; | ||
| } | ||
|
|
||
| export interface FungibleTokenBalance { | ||
| balance: string /* big number as string*/; | ||
| chainId: ChainID; | ||
| tokenDecimals: number; | ||
| } | ||
|
|
||
| export interface SolutionOptions { | ||
| account: string; | ||
| destinationChain: Chain; | ||
| token: FungibleToken; | ||
| amount: bigint; | ||
| threshold?: bigint; | ||
| whitelistedSourceChains: Chain[]; | ||
| account: Address; | ||
| destinationChain: ChainID; | ||
| token: TokenSymbol; | ||
| amount: number; | ||
| threshold?: number; | ||
| whitelistedSourceChains?: ChainID[]; | ||
| } | ||
|
|
||
| interface Amount { | ||
| amount: string; | ||
| amountUSD: number; | ||
| } | ||
|
|
||
| export type SolutionResponse = Solution[] | FailedSolution; | ||
|
|
||
| export interface FailedSolution { | ||
| error: string; | ||
| } | ||
|
|
||
| export interface Solution { | ||
| destinationChain: ChainID; | ||
| destinationTokenAddress: Address; | ||
| duration: number /* estimation duration by seconds */; | ||
| fee: Amount; | ||
| gasCost: Amount; | ||
| senderAddress: Address; | ||
| sourceChain: ChainID; | ||
| sourceTokenAddress: Address; | ||
| amount: string; | ||
| tool: { | ||
| logoURI: string; | ||
| name: string; | ||
| }; | ||
| transaction: Transaction; | ||
| } | ||
|
|
||
| export interface Transaction { | ||
| chainId: ChainID; | ||
| data: string; | ||
| from: Address; | ||
| gasLimit: string; | ||
| gasPrice: string; | ||
| to: Address; | ||
| value: string; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not set process.env instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and you end up in
vitethat does not to useprocess.envbut there isimport.meta.env, made helper method to solve this problem and the setter is in a case you have no options to provide.envlike in React you need use prefixREACT_forenv's