Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions packages/sdk/README.md
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.
3 changes: 2 additions & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@chainsafe/gopher-sdk",
"version": "0.0.1",
"main": "index.js",
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"build": "tsc",
Expand All @@ -13,6 +13,7 @@
"devDependencies": {
"@types/eslint": "^8.37.0",
"@types/node": "18.15.11",
"eip1193-types": "^0.2.1",
"eslint": "^8.37.0",
"typescript": "^5.0.3"
}
Expand Down
105 changes: 105 additions & 0 deletions packages/sdk/src/api.ts
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;
Copy link
Member

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?

Copy link
Contributor Author

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 vite that does not to useprocess.env but there is import.meta.env, made helper method to solve this problem and the setter is in a case you have no options to provide .env like in React you need use prefix REACT_ for env's

}

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;
}
3 changes: 3 additions & 0 deletions packages/sdk/src/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum ChainType {
EVM = "evm",
}
113 changes: 89 additions & 24 deletions packages/sdk/src/index.ts
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());
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we getAvailableTokens if provided tokens array is empty too?

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 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 };
81 changes: 72 additions & 9 deletions packages/sdk/src/types.ts
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;
}
Loading