From f3da187d1159db38783a145f8651d68ff67fb143 Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Thu, 26 Sep 2024 13:06:45 +0200 Subject: [PATCH 01/44] validate /w team --- packages/sdk/package.json | 3 ++- packages/sdk/src/types.ts | 1 + packages/sdk/src/validators.ts | 42 ++++++++++++++++++++++++++++++++++ yarn.lock | 3 ++- 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/sdk/src/validators.ts diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 23bcb4a..cfef315 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -17,7 +17,8 @@ "typescript": "^5.0.3" }, "dependencies": { - "viem": "^2.21.9" + "viem": "^2.21.9", + "zod": "^3.23.8" }, "volta": { "node": "20.17.0", diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index e262668..b671d34 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -52,6 +52,7 @@ export interface ContractCallSolutionOptions { /// outputTokenAddress?: Address; approvalAddress?: Address; + recipient?: Address; } export interface ContractSolutionOptions extends SolutionOptions { diff --git a/packages/sdk/src/validators.ts b/packages/sdk/src/validators.ts new file mode 100644 index 0000000..834b4bf --- /dev/null +++ b/packages/sdk/src/validators.ts @@ -0,0 +1,42 @@ +import { array, number, object, string, union } from "zod"; + +const BridgeCoreSchema = object({ + account: string(), + destinationChain: number(), + token: string(), + amount: number(), + threshold: number().optional(), +}); + +const ContractCallCoreSchema = object({ + callData: string(), + contractAddress: string(), + gasLimit: number(), +}); + +const NativeContractCallSchema = ContractCallCoreSchema.extend({ + recipient: string(), +}); + +const TokenContractCallSchema = ContractCallCoreSchema.extend({ + outputTokenAddress: string().optional(), + approvalAddress: string().optional(), +}); + +export const SingleHopSchema = BridgeCoreSchema.extend({ + sourceChains: number(), // whitelistedSourceChains +}); + +export const MultiHopSchema = BridgeCoreSchema.extend({ + sourceChains: array(number()), // whitelistedSourceChains +}); + +export const SingleHopWithContractSchema = BridgeCoreSchema.extend({ + contractCall: union([NativeContractCallSchema, TokenContractCallSchema]), + sourceChains: number(), // whitelistedSourceChains +}); + +export const MultiHopWithContractSchema = BridgeCoreSchema.extend({ + contractCall: union([NativeContractCallSchema, TokenContractCallSchema]), + sourceChains: array(number()), // whitelistedSourceChains +}); diff --git a/yarn.lock b/yarn.lock index 06c09f8..4c49a09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1870,6 +1870,7 @@ __metadata: eslint: "npm:^8.57.0" typescript: "npm:^5.0.3" viem: "npm:^2.21.9" + zod: "npm:^3.23.8" languageName: unknown linkType: soft @@ -20119,7 +20120,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.21.4, zod@npm:^3.22.3": +"zod@npm:^3.21.4, zod@npm:^3.22.3, zod@npm:^3.23.8": version: 3.23.8 resolution: "zod@npm:3.23.8" checksum: 10c0/8f14c87d6b1b53c944c25ce7a28616896319d95bc46a9660fe441adc0ed0a81253b02b5abdaeffedbeb23bdd25a0bf1c29d2c12dd919aef6447652dd295e3e69 From 47afc402fa70b628cd29fb22e0f1b0bb548852d8 Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Thu, 26 Sep 2024 14:18:57 +0200 Subject: [PATCH 02/44] swap `zod` with `superstruct` --- packages/sdk/package.json | 4 +- packages/sdk/src/validators.ts | 102 ++++++++++++++++++++++++--------- packages/sdk/tsconfig.json | 2 +- yarn.lock | 11 +++- 4 files changed, 86 insertions(+), 33 deletions(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index cfef315..fb6b644 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -17,8 +17,8 @@ "typescript": "^5.0.3" }, "dependencies": { - "viem": "^2.21.9", - "zod": "^3.23.8" + "superstruct": "^2.0.2", + "viem": "^2.21.9" }, "volta": { "node": "20.17.0", diff --git a/packages/sdk/src/validators.ts b/packages/sdk/src/validators.ts index 834b4bf..542913d 100644 --- a/packages/sdk/src/validators.ts +++ b/packages/sdk/src/validators.ts @@ -1,42 +1,88 @@ -import { array, number, object, string, union } from "zod"; +import { + array, + assign, + bigint, + define, + number, + object, + optional, + refine, + string, + type Struct, + union, +} from "superstruct"; + +const hexString = (): Struct => + define("hexString", (value) => { + if (typeof value !== "string") return false; + const hexRegex = /^0x[0-9a-fA-F]+$/; + return hexRegex.test(value); + }); + +const numberLike = refine( + union([number(), hexString(), bigint()]), + "numberLike", + (value) => { + if (typeof value === "string") return !isNaN(Number(value)); + return true; // If it's a number or bigint, it's already valid + }, +); const BridgeCoreSchema = object({ - account: string(), + account: hexString(), destinationChain: number(), token: string(), - amount: number(), - threshold: number().optional(), + amount: numberLike, + threshold: optional(number()), }); const ContractCallCoreSchema = object({ - callData: string(), - contractAddress: string(), - gasLimit: number(), + callData: hexString(), + contractAddress: hexString(), + gasLimit: numberLike, }); -const NativeContractCallSchema = ContractCallCoreSchema.extend({ - recipient: string(), -}); +const NativeContractCallSchema = assign( + ContractCallCoreSchema, + object({ + recipient: hexString(), + }), +); -const TokenContractCallSchema = ContractCallCoreSchema.extend({ - outputTokenAddress: string().optional(), - approvalAddress: string().optional(), -}); +const TokenContractCallSchema = assign( + ContractCallCoreSchema, + object({ + outputTokenAddress: optional(hexString()), + approvalAddress: optional(hexString()), + }), +); -export const SingleHopSchema = BridgeCoreSchema.extend({ - sourceChains: number(), // whitelistedSourceChains -}); +export const SingleHopSchema = assign( + BridgeCoreSchema, + object({ + sourceChains: number(), // whitelistedSourceChains + }), +); -export const MultiHopSchema = BridgeCoreSchema.extend({ - sourceChains: array(number()), // whitelistedSourceChains -}); +export const MultiHopSchema = assign( + BridgeCoreSchema, + object({ + sourceChains: array(number()), // whitelistedSourceChains + }), +); -export const SingleHopWithContractSchema = BridgeCoreSchema.extend({ - contractCall: union([NativeContractCallSchema, TokenContractCallSchema]), - sourceChains: number(), // whitelistedSourceChains -}); +export const SingleHopWithContractSchema = assign( + BridgeCoreSchema, + object({ + contractCall: union([NativeContractCallSchema, TokenContractCallSchema]), + sourceChains: number(), // whitelistedSourceChains + }), +); -export const MultiHopWithContractSchema = BridgeCoreSchema.extend({ - contractCall: union([NativeContractCallSchema, TokenContractCallSchema]), - sourceChains: array(number()), // whitelistedSourceChains -}); +export const MultiHopWithContractSchema = assign( + BridgeCoreSchema, + object({ + contractCall: union([NativeContractCallSchema, TokenContractCallSchema]), + sourceChains: array(number()), // whitelistedSourceChains + }), +); diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 0564f0c..b27439e 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -78,7 +78,7 @@ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ diff --git a/yarn.lock b/yarn.lock index 4c49a09..440cabd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1868,9 +1868,9 @@ __metadata: "@types/eslint": "npm:^8.56.11" "@types/node": "npm:18.19.42" eslint: "npm:^8.57.0" + superstruct: "npm:^2.0.2" typescript: "npm:^5.0.3" viem: "npm:^2.21.9" - zod: "npm:^3.23.8" languageName: unknown linkType: soft @@ -17940,6 +17940,13 @@ __metadata: languageName: node linkType: hard +"superstruct@npm:^2.0.2": + version: 2.0.2 + resolution: "superstruct@npm:2.0.2" + checksum: 10c0/c6853db5240b4920f47b3c864dd1e23ede6819ea399ad29a65387d746374f6958c5f1c5b7e5bb152d9db117a74973e5005056d9bb83c24e26f18ec6bfae4a718 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -20120,7 +20127,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.21.4, zod@npm:^3.22.3, zod@npm:^3.23.8": +"zod@npm:^3.21.4, zod@npm:^3.22.3": version: 3.23.8 resolution: "zod@npm:3.23.8" checksum: 10c0/8f14c87d6b1b53c944c25ce7a28616896319d95bc46a9660fe441adc0ed0a81253b02b5abdaeffedbeb23bdd25a0bf1c29d2c12dd919aef6447652dd295e3e69 From 39568634b92c57644db0dee90767eb98636297a4 Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Thu, 26 Sep 2024 15:11:40 +0200 Subject: [PATCH 03/44] first case for api refactor --- packages/sdk/src/api.ts | 6 +-- packages/sdk/src/index.ts | 88 ++++++++++++++++++++++++++++++++-- packages/sdk/src/types.ts | 8 +++- packages/sdk/src/validators.ts | 10 ++-- 4 files changed, 98 insertions(+), 14 deletions(-) diff --git a/packages/sdk/src/api.ts b/packages/sdk/src/api.ts index 9f78493..e6db922 100644 --- a/packages/sdk/src/api.ts +++ b/packages/sdk/src/api.ts @@ -1,4 +1,4 @@ -import type { +import { Address, Chain, ChainID, @@ -7,7 +7,7 @@ import type { FetchOptions, FungibleToken, FungibleTokenBalance, - NativeTokenBalance, + NativeTokenBalance, SingleHopContractSolutionOptions, Solution, SolutionOptions, SolutionResponse, @@ -183,7 +183,7 @@ export async function getContractCallSolution( contractCall, threshold, whitelistedSourceChains, - }: ContractSolutionOptions, + }: SingleHopContractSolutionOptions, { baseUrl, signal }: FetchOptions = {}, ): Promise { const url = new URL("/solution/call", baseUrl || BASE_URL); diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index b1841ea..3ea5b7c 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,3 +1,6 @@ +import type { Infer } from "superstruct"; +import { assert } from "superstruct"; + import { getFungibleTokens, getSolution, @@ -16,11 +19,18 @@ import type { ContractSolutionOptions, FetchOptions, FungibleToken, + SingleHopContractSolutionOptions, SolutionOptions, SolutionResponse, TokenBalance, TokenSymbol, } from "./types"; +import { + MultiHopSchema, + MultiHopWithContractSchema, + SingleHopSchema, + SingleHopWithContractSchema, +} from "./validators"; export type * from "./types"; export * as api from "./api"; @@ -108,15 +118,83 @@ class Sprinter { ); } - public async getSolution( + public async bridgeAggregateBalance( + settings: Infer, + options?: FetchOptions, + ): Promise { + assert(settings, MultiHopSchema); + + const { sourceChains, amount, ...data } = settings; + return this.getSolution( + { + ...data, + amount: BigInt(amount), + whitelistedSourceChains: sourceChains, + } as SolutionOptions, + options, + ); + } + + public async bridgeAggregateBalanceAndCall( + settings: Infer, + options?: FetchOptions, + ): Promise { + assert(settings, MultiHopWithContractSchema); + + const { sourceChains, amount, ...data } = settings; + return this.getSolution( + { + ...data, + amount: BigInt(amount), + whitelistedSourceChains: sourceChains, + } as SolutionOptions, + options, + ); + } + + public async bridge( + settings: Infer, + options?: FetchOptions, + ): Promise { + assert(settings, SingleHopSchema); + + const { sourceChains, amount, ...data } = settings; + return this.getSingleSolution( + { + ...data, + amount: BigInt(amount), + whitelistedSourceChains: sourceChains ? [sourceChains] : [], + } as SolutionOptions, + options, + ); + } + + public async bridgeAndCall( + settings: Infer, + options?: FetchOptions, + ): Promise { + assert(settings, SingleHopWithContractSchema); + + const { sourceChains, amount, ...data } = settings; + return this.getSingleSolution( + { + ...data, + amount: BigInt(amount), + whitelistedSourceChains: sourceChains ? [sourceChains] : [], + } as SolutionOptions, + options, + ); + } + + private async getSolution( settings: ContractSolutionOptions, options?: FetchOptions, ): Promise; - public async getSolution( + private async getSolution( settings: SolutionOptions, options?: FetchOptions, ): Promise; - public async getSolution( + private async getSolution( settings: unknown, options?: FetchOptions, ): Promise { @@ -134,8 +212,8 @@ class Sprinter { ); } - public async getCallSolution( - settings: ContractSolutionOptions, + private async getSingleSolution( + settings: SingleHopContractSolutionOptions, options?: FetchOptions, ): Promise { if (typeof settings !== "object" || settings === null) diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index b671d34..7787a58 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -36,11 +36,13 @@ export type FungibleTokenBalance = TokenBalance; export type NativeTokenBalance = TokenBalance; +export type NumberLike = number | string | bigint; + export interface SolutionOptions { account: Address; destinationChain: ChainID; token: TokenSymbol; - amount: number; + amount: NumberLike; threshold?: number; whitelistedSourceChains?: ChainID[]; } @@ -55,6 +57,10 @@ export interface ContractCallSolutionOptions { recipient?: Address; } +export interface SingleHopContractSolutionOptions extends SolutionOptions { + contractCall?: ContractCallSolutionOptions; +} + export interface ContractSolutionOptions extends SolutionOptions { contractCall: ContractCallSolutionOptions; } diff --git a/packages/sdk/src/validators.ts b/packages/sdk/src/validators.ts index 542913d..258015a 100644 --- a/packages/sdk/src/validators.ts +++ b/packages/sdk/src/validators.ts @@ -12,7 +12,7 @@ import { union, } from "superstruct"; -const hexString = (): Struct => +const hexString = (): Struct => define("hexString", (value) => { if (typeof value !== "string") return false; const hexRegex = /^0x[0-9a-fA-F]+$/; @@ -60,14 +60,14 @@ const TokenContractCallSchema = assign( export const SingleHopSchema = assign( BridgeCoreSchema, object({ - sourceChains: number(), // whitelistedSourceChains + sourceChains: optional(number()), // whitelistedSourceChains }), ); export const MultiHopSchema = assign( BridgeCoreSchema, object({ - sourceChains: array(number()), // whitelistedSourceChains + sourceChains: optional(array(number())), // whitelistedSourceChains }), ); @@ -75,7 +75,7 @@ export const SingleHopWithContractSchema = assign( BridgeCoreSchema, object({ contractCall: union([NativeContractCallSchema, TokenContractCallSchema]), - sourceChains: number(), // whitelistedSourceChains + sourceChains: optional(number()), // whitelistedSourceChains }), ); @@ -83,6 +83,6 @@ export const MultiHopWithContractSchema = assign( BridgeCoreSchema, object({ contractCall: union([NativeContractCallSchema, TokenContractCallSchema]), - sourceChains: array(number()), // whitelistedSourceChains + sourceChains: optional(array(number())), // whitelistedSourceChains }), ); From ce3b88c089259d2357bcb70d9cef0d7b4dc096a3 Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Thu, 26 Sep 2024 15:52:04 +0200 Subject: [PATCH 04/44] move sprinter to separate file --- packages/sdk/src/index.ts | 247 +---------------------------------- packages/sdk/src/sprinter.ts | 240 ++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 246 deletions(-) create mode 100644 packages/sdk/src/sprinter.ts diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 3ea5b7c..3a0ea35 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,249 +1,4 @@ -import type { Infer } from "superstruct"; -import { assert } from "superstruct"; - -import { - getFungibleTokens, - getSolution, - getContractSolution, - getSupportedChains, - getUserFungibleTokens, - getUserNativeTokens, - setBaseUrl, - BASE_URL, - getContractCallSolution, -} from "./api"; -import type { - Address, - AggregateBalances, - Chain, - ContractSolutionOptions, - FetchOptions, - FungibleToken, - SingleHopContractSolutionOptions, - SolutionOptions, - SolutionResponse, - TokenBalance, - TokenSymbol, -} from "./types"; -import { - MultiHopSchema, - MultiHopWithContractSchema, - SingleHopSchema, - SingleHopWithContractSchema, -} from "./validators"; - export type * from "./types"; export * as api from "./api"; export * from "./enums"; - -class Sprinter { - // in memory "cache" - #tokens?: FungibleToken[]; - #chains?: Chain[]; - #requests: Record> = {}; - - #fetchOptions: Omit; - - constructor(fetchOptions: Omit = {}) { - this.#fetchOptions = fetchOptions; - } - - public async getAvailableTokens( - options: FetchOptions = {}, - ): Promise { - if (!this.#tokens) - this.#tokens = await this.deferredRequest("tokens", () => - getFungibleTokens(this.makeFetchOptions(options)), - ); - return this.#tokens; - } - - public async getAvailableChains( - options: FetchOptions = {}, - ): Promise { - if (!this.#chains) - this.#chains = await this.deferredRequest("chains", () => - getSupportedChains(this.makeFetchOptions(options)), - ); - return this.#chains; - } - - public async getUserBalances( - account: Address, - tokens?: FungibleToken[], - options: FetchOptions = {}, - ): Promise { - const tokenList = tokens || (await this.getAvailableTokens(options)); - - const [balances, nativeTokens] = await this.deferredRequest( - `balances-${account}`, - () => - Promise.all([ - Promise.all( - tokenList.map((token) => - getUserFungibleTokens(account, token.symbol, options).then( - (balances) => ({ - symbol: token.symbol, - balances, - }), - ), - ), - ), - getUserNativeTokens(account, options), - ]), - ); - return balances.reduce( - (previousValue, { symbol, balances }) => { - previousValue[symbol] = { - total: balances - .reduce((prev, cur) => prev + BigInt(cur.balance), 0n) - .toString(), - balances, - }; - return previousValue; - }, - { - ["ETH"]: { - total: nativeTokens - .reduce((prev, cur) => prev + BigInt(cur.balance), 0n) - .toString(), - balances: nativeTokens, - }, - } as { - [symbol: TokenSymbol]: { - balances: TokenBalance[]; - total: string; - }; - }, - ); - } - - public async bridgeAggregateBalance( - settings: Infer, - options?: FetchOptions, - ): Promise { - assert(settings, MultiHopSchema); - - const { sourceChains, amount, ...data } = settings; - return this.getSolution( - { - ...data, - amount: BigInt(amount), - whitelistedSourceChains: sourceChains, - } as SolutionOptions, - options, - ); - } - - public async bridgeAggregateBalanceAndCall( - settings: Infer, - options?: FetchOptions, - ): Promise { - assert(settings, MultiHopWithContractSchema); - - const { sourceChains, amount, ...data } = settings; - return this.getSolution( - { - ...data, - amount: BigInt(amount), - whitelistedSourceChains: sourceChains, - } as SolutionOptions, - options, - ); - } - - public async bridge( - settings: Infer, - options?: FetchOptions, - ): Promise { - assert(settings, SingleHopSchema); - - const { sourceChains, amount, ...data } = settings; - return this.getSingleSolution( - { - ...data, - amount: BigInt(amount), - whitelistedSourceChains: sourceChains ? [sourceChains] : [], - } as SolutionOptions, - options, - ); - } - - public async bridgeAndCall( - settings: Infer, - options?: FetchOptions, - ): Promise { - assert(settings, SingleHopWithContractSchema); - - const { sourceChains, amount, ...data } = settings; - return this.getSingleSolution( - { - ...data, - amount: BigInt(amount), - whitelistedSourceChains: sourceChains ? [sourceChains] : [], - } as SolutionOptions, - options, - ); - } - - private async getSolution( - settings: ContractSolutionOptions, - options?: FetchOptions, - ): Promise; - private async getSolution( - settings: SolutionOptions, - options?: FetchOptions, - ): Promise; - private async getSolution( - settings: unknown, - options?: FetchOptions, - ): Promise { - if (typeof settings !== "object" || settings === null) - throw new Error("Missing settings object"); - - if ("contractCall" in settings) - return await getContractSolution( - settings, - this.makeFetchOptions(options || {}), - ); - return await getSolution( - settings, - this.makeFetchOptions(options || {}), - ); - } - - private async getSingleSolution( - settings: SingleHopContractSolutionOptions, - options?: FetchOptions, - ): Promise { - if (typeof settings !== "object" || settings === null) - throw new Error("Missing settings object"); - - return await getContractCallSolution( - settings, - this.makeFetchOptions(options || {}), - ); - } - - private deferredRequest( - name: string, - request: () => Promise, - ): Promise { - if (!(name in this.#requests)) { - this.#requests[name] = request(); - void this.#requests[name].finally(() => { - void setTimeout(() => { - delete this.#requests[name]; - }, 1000); - }); - } - - return this.#requests[name] as Promise; - } - - private makeFetchOptions(options: FetchOptions): FetchOptions { - return { ...this.#fetchOptions, ...options }; - } -} - -export { Sprinter, setBaseUrl, BASE_URL }; +export { Sprinter } from "./sprinter"; diff --git a/packages/sdk/src/sprinter.ts b/packages/sdk/src/sprinter.ts new file mode 100644 index 0000000..399013a --- /dev/null +++ b/packages/sdk/src/sprinter.ts @@ -0,0 +1,240 @@ +import type { Infer } from "superstruct"; +import { assert } from "superstruct"; + +import { + getContractCallSolution, + getContractSolution, + getFungibleTokens, + getSupportedChains, + getUserFungibleTokens, + getUserNativeTokens, +} from "./api"; +import type { + Address, + AggregateBalances, + Chain, + ContractSolutionOptions, + FetchOptions, + FungibleToken, + SingleHopContractSolutionOptions, + SolutionOptions, + SolutionResponse, + TokenBalance, + TokenSymbol, +} from "./types"; +import { + MultiHopSchema, + MultiHopWithContractSchema, + SingleHopSchema, + SingleHopWithContractSchema, +} from "./validators"; + +export class Sprinter { + // in memory "cache" + #tokens?: FungibleToken[]; + #chains?: Chain[]; + #requests: Record> = {}; + + #fetchOptions: Omit; + + constructor(fetchOptions: Omit = {}) { + this.#fetchOptions = fetchOptions; + } + + public async getAvailableTokens( + options: FetchOptions = {}, + ): Promise { + if (!this.#tokens) + this.#tokens = await this.deferredRequest("tokens", () => + getFungibleTokens(this.makeFetchOptions(options)), + ); + return this.#tokens; + } + + public async getAvailableChains( + options: FetchOptions = {}, + ): Promise { + if (!this.#chains) + this.#chains = await this.deferredRequest("chains", () => + getSupportedChains(this.makeFetchOptions(options)), + ); + return this.#chains; + } + + public async getUserBalances( + account: Address, + tokens?: FungibleToken[], + options: FetchOptions = {}, + ): Promise { + const tokenList = tokens || (await this.getAvailableTokens(options)); + + const [balances, nativeTokens] = await this.deferredRequest( + `balances-${account}`, + () => + Promise.all([ + Promise.all( + tokenList.map((token) => + getUserFungibleTokens(account, token.symbol, options).then( + (balances) => ({ + symbol: token.symbol, + balances, + }), + ), + ), + ), + getUserNativeTokens(account, options), + ]), + ); + return balances.reduce( + (previousValue, { symbol, balances }) => { + previousValue[symbol] = { + total: balances + .reduce((prev, cur) => prev + BigInt(cur.balance), 0n) + .toString(), + balances, + }; + return previousValue; + }, + { + ["ETH"]: { + total: nativeTokens + .reduce((prev, cur) => prev + BigInt(cur.balance), 0n) + .toString(), + balances: nativeTokens, + }, + } as { + [symbol: TokenSymbol]: { + balances: TokenBalance[]; + total: string; + }; + }, + ); + } + + public async bridgeAggregateBalance( + settings: Infer, + options?: FetchOptions, + ): Promise { + assert(settings, MultiHopSchema); + + const { sourceChains, amount, ...data } = settings; + return this.getSolution( + { + ...data, + amount: BigInt(amount), + whitelistedSourceChains: sourceChains, + } as SolutionOptions, + options, + ); + } + + public async bridgeAggregateBalanceAndCall( + settings: Infer, + options?: FetchOptions, + ): Promise { + assert(settings, MultiHopWithContractSchema); + + const { sourceChains, amount, ...data } = settings; + return this.getSolution( + { + ...data, + amount: BigInt(amount), + whitelistedSourceChains: sourceChains, + } as SolutionOptions, + options, + ); + } + + public async bridge( + settings: Infer, + options?: FetchOptions, + ): Promise { + assert(settings, SingleHopSchema); + + const { sourceChains, amount, ...data } = settings; + return this.getSingleSolution( + { + ...data, + amount: BigInt(amount), + whitelistedSourceChains: sourceChains ? [sourceChains] : [], + } as SolutionOptions, + options, + ); + } + + public async bridgeAndCall( + settings: Infer, + options?: FetchOptions, + ): Promise { + assert(settings, SingleHopWithContractSchema); + + const { sourceChains, amount, ...data } = settings; + return this.getSingleSolution( + { + ...data, + amount: BigInt(amount), + whitelistedSourceChains: sourceChains ? [sourceChains] : [], + } as SolutionOptions, + options, + ); + } + + private async getSolution( + settings: ContractSolutionOptions, + options?: FetchOptions, + ): Promise; + private async getSolution( + settings: SolutionOptions, + options?: FetchOptions, + ): Promise; + private async getSolution( + settings: unknown, + options?: FetchOptions, + ): Promise { + if (typeof settings !== "object" || settings === null) + throw new Error("Missing settings object"); + + if ("contractCall" in settings) + return await getContractSolution( + settings, + this.makeFetchOptions(options || {}), + ); + return await getSolution( + settings, + this.makeFetchOptions(options || {}), + ); + } + + private async getSingleSolution( + settings: SingleHopContractSolutionOptions, + options?: FetchOptions, + ): Promise { + if (typeof settings !== "object" || settings === null) + throw new Error("Missing settings object"); + + return await getContractCallSolution( + settings, + this.makeFetchOptions(options || {}), + ); + } + + private deferredRequest( + name: string, + request: () => Promise, + ): Promise { + if (!(name in this.#requests)) { + this.#requests[name] = request(); + void this.#requests[name].finally(() => { + void setTimeout(() => { + delete this.#requests[name]; + }, 1000); + }); + } + + return this.#requests[name] as Promise; + } + + private makeFetchOptions(options: FetchOptions): FetchOptions { + return { ...this.#fetchOptions, ...options }; + } +} From db2a9cf53e1eaf0e629354905f9f3669841abf86 Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Thu, 26 Sep 2024 16:34:22 +0200 Subject: [PATCH 05/44] refactor --- packages/sdk/src/internal/userBalances.ts | 66 +++++++++++ packages/sdk/src/{ => internal}/validators.ts | 0 packages/sdk/src/sprinter.ts | 111 +++--------------- 3 files changed, 85 insertions(+), 92 deletions(-) create mode 100644 packages/sdk/src/internal/userBalances.ts rename packages/sdk/src/{ => internal}/validators.ts (100%) diff --git a/packages/sdk/src/internal/userBalances.ts b/packages/sdk/src/internal/userBalances.ts new file mode 100644 index 0000000..2bc2892 --- /dev/null +++ b/packages/sdk/src/internal/userBalances.ts @@ -0,0 +1,66 @@ +import { getUserFungibleTokens, getUserNativeTokens } from "../api"; +import type { + Address, + AggregateBalances, + FetchOptions, + FungibleToken, + FungibleTokenBalance, + NativeTokenBalance, + TokenBalance, + TokenSymbol, +} from "../types"; + +type FetchBalancesResponse = [ + { symbol: TokenSymbol; balances: FungibleTokenBalance[] }[], + NativeTokenBalance[], +]; + +export function getUserBalances( + account: Address, + tokens: FungibleToken[], + options: FetchOptions, +): Promise { + return Promise.all([ + Promise.all( + tokens.map((token) => + getUserFungibleTokens(account, token.symbol, options).then( + (balances) => ({ + symbol: token.symbol, + balances, + }), + ), + ), + ), + getUserNativeTokens(account, options), + ]); +} + +export function formatBalances([ + balances, + nativeTokens, +]: FetchBalancesResponse): AggregateBalances { + return balances.reduce( + (previousValue, { symbol, balances }) => { + previousValue[symbol] = { + total: balances + .reduce((prev, cur) => prev + BigInt(cur.balance), 0n) + .toString(), + balances, + }; + return previousValue; + }, + { + ["ETH"]: { + total: nativeTokens + .reduce((prev, cur) => prev + BigInt(cur.balance), 0n) + .toString(), + balances: nativeTokens, + }, + } as { + [symbol: TokenSymbol]: { + balances: TokenBalance[]; + total: string; + }; + }, + ); +} diff --git a/packages/sdk/src/validators.ts b/packages/sdk/src/internal/validators.ts similarity index 100% rename from packages/sdk/src/validators.ts rename to packages/sdk/src/internal/validators.ts diff --git a/packages/sdk/src/sprinter.ts b/packages/sdk/src/sprinter.ts index 399013a..db8eaa3 100644 --- a/packages/sdk/src/sprinter.ts +++ b/packages/sdk/src/sprinter.ts @@ -5,10 +5,16 @@ import { getContractCallSolution, getContractSolution, getFungibleTokens, + getSolution, getSupportedChains, - getUserFungibleTokens, - getUserNativeTokens, } from "./api"; +import { formatBalances, getUserBalances } from "./internal/userBalances"; +import { + MultiHopSchema, + MultiHopWithContractSchema, + SingleHopSchema, + SingleHopWithContractSchema, +} from "./internal/validators"; import type { Address, AggregateBalances, @@ -16,18 +22,9 @@ import type { ContractSolutionOptions, FetchOptions, FungibleToken, - SingleHopContractSolutionOptions, SolutionOptions, SolutionResponse, - TokenBalance, - TokenSymbol, } from "./types"; -import { - MultiHopSchema, - MultiHopWithContractSchema, - SingleHopSchema, - SingleHopWithContractSchema, -} from "./validators"; export class Sprinter { // in memory "cache" @@ -71,44 +68,13 @@ export class Sprinter { const [balances, nativeTokens] = await this.deferredRequest( `balances-${account}`, () => - Promise.all([ - Promise.all( - tokenList.map((token) => - getUserFungibleTokens(account, token.symbol, options).then( - (balances) => ({ - symbol: token.symbol, - balances, - }), - ), - ), - ), - getUserNativeTokens(account, options), - ]), - ); - return balances.reduce( - (previousValue, { symbol, balances }) => { - previousValue[symbol] = { - total: balances - .reduce((prev, cur) => prev + BigInt(cur.balance), 0n) - .toString(), - balances, - }; - return previousValue; - }, - { - ["ETH"]: { - total: nativeTokens - .reduce((prev, cur) => prev + BigInt(cur.balance), 0n) - .toString(), - balances: nativeTokens, - }, - } as { - [symbol: TokenSymbol]: { - balances: TokenBalance[]; - total: string; - }; - }, + getUserBalances( + account, + tokenList, + this.makeFetchOptions(options || {}), + ), ); + return formatBalances([balances, nativeTokens]); } public async bridgeAggregateBalance( @@ -118,7 +84,7 @@ export class Sprinter { assert(settings, MultiHopSchema); const { sourceChains, amount, ...data } = settings; - return this.getSolution( + return await getSolution( { ...data, amount: BigInt(amount), @@ -135,12 +101,12 @@ export class Sprinter { assert(settings, MultiHopWithContractSchema); const { sourceChains, amount, ...data } = settings; - return this.getSolution( + return await getContractSolution( { ...data, amount: BigInt(amount), whitelistedSourceChains: sourceChains, - } as SolutionOptions, + } as ContractSolutionOptions, options, ); } @@ -152,7 +118,7 @@ export class Sprinter { assert(settings, SingleHopSchema); const { sourceChains, amount, ...data } = settings; - return this.getSingleSolution( + return await getContractCallSolution( { ...data, amount: BigInt(amount), @@ -169,7 +135,7 @@ export class Sprinter { assert(settings, SingleHopWithContractSchema); const { sourceChains, amount, ...data } = settings; - return this.getSingleSolution( + return await getContractCallSolution( { ...data, amount: BigInt(amount), @@ -179,45 +145,6 @@ export class Sprinter { ); } - private async getSolution( - settings: ContractSolutionOptions, - options?: FetchOptions, - ): Promise; - private async getSolution( - settings: SolutionOptions, - options?: FetchOptions, - ): Promise; - private async getSolution( - settings: unknown, - options?: FetchOptions, - ): Promise { - if (typeof settings !== "object" || settings === null) - throw new Error("Missing settings object"); - - if ("contractCall" in settings) - return await getContractSolution( - settings, - this.makeFetchOptions(options || {}), - ); - return await getSolution( - settings, - this.makeFetchOptions(options || {}), - ); - } - - private async getSingleSolution( - settings: SingleHopContractSolutionOptions, - options?: FetchOptions, - ): Promise { - if (typeof settings !== "object" || settings === null) - throw new Error("Missing settings object"); - - return await getContractCallSolution( - settings, - this.makeFetchOptions(options || {}), - ); - } - private deferredRequest( name: string, request: () => Promise, From 18f768b15a7a4de20a64991f4930bd84fb512269 Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Mon, 30 Sep 2024 15:45:05 +0200 Subject: [PATCH 06/44] smoll fixes --- packages/sdk/src/internal/userBalances.ts | 8 +------- packages/sdk/src/internal/validators.ts | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/internal/userBalances.ts b/packages/sdk/src/internal/userBalances.ts index 2bc2892..bd0b80c 100644 --- a/packages/sdk/src/internal/userBalances.ts +++ b/packages/sdk/src/internal/userBalances.ts @@ -6,7 +6,6 @@ import type { FungibleToken, FungibleTokenBalance, NativeTokenBalance, - TokenBalance, TokenSymbol, } from "../types"; @@ -56,11 +55,6 @@ export function formatBalances([ .toString(), balances: nativeTokens, }, - } as { - [symbol: TokenSymbol]: { - balances: TokenBalance[]; - total: string; - }; - }, + } as AggregateBalances, ); } diff --git a/packages/sdk/src/internal/validators.ts b/packages/sdk/src/internal/validators.ts index 258015a..f394fc6 100644 --- a/packages/sdk/src/internal/validators.ts +++ b/packages/sdk/src/internal/validators.ts @@ -60,7 +60,7 @@ const TokenContractCallSchema = assign( export const SingleHopSchema = assign( BridgeCoreSchema, object({ - sourceChains: optional(number()), // whitelistedSourceChains + sourceChain: optional(number()), // whitelistedSourceChains }), ); @@ -75,7 +75,7 @@ export const SingleHopWithContractSchema = assign( BridgeCoreSchema, object({ contractCall: union([NativeContractCallSchema, TokenContractCallSchema]), - sourceChains: optional(number()), // whitelistedSourceChains + sourceChain: optional(number()), // whitelistedSourceChains }), ); From b1151a8633d8355e5bc80d9a71066ea7c283d64c Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Mon, 30 Sep 2024 15:45:12 +0200 Subject: [PATCH 07/44] sprinter class docs --- packages/sdk/src/sprinter.ts | 341 ++++++++++++++++++++++++++++++++++- 1 file changed, 337 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/sprinter.ts b/packages/sdk/src/sprinter.ts index db8eaa3..4da49fd 100644 --- a/packages/sdk/src/sprinter.ts +++ b/packages/sdk/src/sprinter.ts @@ -38,6 +38,24 @@ export class Sprinter { this.#fetchOptions = fetchOptions; } + /** + * Fetches and returns the list of fungible tokens supported by Sprinter. + * + * @param {FetchOptions} [options={}] - Optional configuration for the fetch request, which can include custom headers or query parameters. + * + * @returns {Promise} A promise that resolves to a list of fungible tokens. + * + * @example + * ```ts + * import { Sprinter } from '@chainsafe/sprinter-sdk'; + * + * const sprinter = new Sprinter(); + * + * sprinter.getAvailableTokens().then(tokens => { + * console.log(tokens); + * }); + * ``` + */ public async getAvailableTokens( options: FetchOptions = {}, ): Promise { @@ -48,6 +66,24 @@ export class Sprinter { return this.#tokens; } + /** + * Fetches and returns the list of supported blockchain networks (chains) available in Sprinter. + * + * @param {FetchOptions} [options={}] - Optional configuration for the fetch request, which can include custom headers or query parameters. + * + * @returns {Promise} A promise that resolves to a list of supported chains. + * + * @example + * ```ts + * import { Sprinter } from '@chainsafe/sprinter-sdk'; + * + * const sprinter = new Sprinter(); + * + * sprinter.getAvailableChains().then(chains => { + * console.log(chains); + * }); + * ``` + */ public async getAvailableChains( options: FetchOptions = {}, ): Promise { @@ -58,6 +94,60 @@ export class Sprinter { return this.#chains; } + /** + * Fetches and returns the aggregate balances of the specified user across the provided list of tokens. + * + * @param {Address} account - The user's wallet address for which balances are to be fetched. + * @param {FungibleToken[]} [tokens] - An optional list of tokens to check balances for. If an empty array is provided, only native token balances (e.g., ETH) will be returned. + * @param {FetchOptions} [options={}] - Optional configuration for the fetch request, which can include custom headers or query parameters. + * + * @returns {Promise} A promise that resolves to the user's aggregate balances for the specified tokens and always includes native token balances (e.g., ETH). + * + * @note If no tokens are provided or if an empty array is passed, the function will only return the native token balances (e.g., ETH). However, native balances are always returned regardless of the tokens specified. + * + * @example + * ```ts + * import { Sprinter } from '@chainsafe/sprinter-sdk'; + * + * const sprinter = new Sprinter(); + * const account = "0xYourAddressHere"; + * + * sprinter.getUserBalances(account).then(balances => { + * console.log(balances); + * }); + * ``` + * + * Returned Example: + * ```json + * { + * "USDC": { + * "balances": [ + * { + * "balance": "100000000", // Balance in smallest denomination + * "chainId": 1, + * "tokenDecimals": 6 + * }, + * { + * "balance": "5000000", // Balance in smallest denomination + * "chainId": 137, + * "tokenDecimals": 6 + * } + * ], + * "total": "105000000" // Total balance across all chains + * }, + * "ETH": { + * "balances": [ + * { + * "balance": "2000000000000000000", // 2 ETH + * "chainId": 1, + * "tokenDecimals": 18 + * } + * ], + * "total": "2000000000000000000" // Total balance across all chains + * } + * } + * ``` + */ public async getUserBalances( account: Address, tokens?: FungibleToken[], @@ -77,6 +167,90 @@ export class Sprinter { return formatBalances([balances, nativeTokens]); } + /** + * Fetches and returns the optimal bridging solution based on the provided settings for bridging tokens between multiple chains. + * + * This method uses the provided settings to determine the best path for transferring tokens across multiple supported blockchains using a multi-hop strategy. + * + * @param {Infer} settings - The settings object for defining the bridging parameters: + * - `account`: The user's wallet address for the transaction. + * - `destinationChain`: The ID of the destination blockchain. + * - `token`: The token symbol (e.g., "ETH", "USDC") to be bridged. + * - `amount`: The amount of tokens to bridge (as a string or number). + * - `threshold` (optional): The minimum amount threshold required for bridging. + * - `sourceChains` (optional): An array of whitelisted source chain IDs for the transfer. + * + * @param {FetchOptions} [options] - Optional configuration for the fetch request, such as custom headers or query parameters. + * + * @returns {Promise} A promise that resolves to an array of possible bridging solutions (`Solution[]`), or a `FailedSolution` object in case of an error. + * + * @example + * ```ts + * import { Sprinter } from '@chainsafe/sprinter-sdk'; + * + * const sprinter = new Sprinter(); + * const settings = { + * account: "0x3e101ec02e7a48d16dade204c96bff842e7e2519", + * destinationChain: 11155111, + * token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + * amount: "100000000", + * sourceChains: [84532, 137], + * }; + * + * sprinter.bridgeAggregateBalance(settings).then(solution => { + * console.log(solution); + * }); + * ``` + * + * Returned Example: + * ```json + * [ + * { + * "sourceChain": 84532, + * "destinationChain": 11155111, + * "sourceTokenAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + * "destinationTokenAddress": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + * "senderAddress": "0x3e101ec02e7a48d16dade204c96bff842e7e2519", + * "tool": { + * "name": "Sygma-Testnet", + * "logoURI": "https://scan.buildwithsygma.com/assets/images/logo1.svg" + * }, + * "gasCost": { + * "amount": "221055913000", + * "amountUSD": 0 + * }, + * "fee": { + * "amount": "1000000000000000", + * "amountUSD": 0 + * }, + * "amount": "100000000", + * "duration": 60000000000, + * "transaction": { + * "data": "0x73c45c98000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000540000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000143e101ec02e7a48d16dade204c96bff842e7e251900000000000000000000000000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000", + * "to": "0x9D5C332Ebe0DaE36e07a4eD552Ad4d8c5067A61F", + * "from": "0x3E101Ec02e7A48D16DADE204C96bFF842E7E2519", + * "value": "0x38d7ea4c68000", + * "gasPrice": "0xf433d", + * "gasLimit": "0x35f48", + * "chainId": 84532 + * }, + * "approvals": [ + * { + * "data": "0x095ea7b30000000000000000000000003b0f996c474c91de56617da13a52b22bb659d18e0000000000000000000000000000000000000000000000000000000005f5e100", + * "to": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + * "from": "0x3E101Ec02e7A48D16DADE204C96bFF842E7E2519", + * "value": "0x0", + * "gasPrice": "0xf433d", + * "gasLimit": "0xe484", + * "chainId": 84532 + * } + * ] + * } + * ] + * ``` + * + * @note If the bridging process encounters an error, the returned object will be of type `FailedSolution` containing an error message. + */ public async bridgeAggregateBalance( settings: Infer, options?: FetchOptions, @@ -94,10 +268,71 @@ export class Sprinter { ); } + /** + * Fetches and returns the optimal bridging solution and performs a contract call on the destination chain. + * + * This method is intended to determine the best path for transferring tokens across multiple supported blockchains + * and then execute a contract call on the destination chain using either native or token contract interactions. + * + * @param {Infer} settings - The settings object for defining the bridging and contract call parameters: + * - `account`: The user's wallet address for the transaction. + * - `destinationChain`: The ID of the destination blockchain. + * - `token`: The token symbol (e.g., "ETH", "USDC") to be bridged. + * - `amount`: The amount of tokens to bridge (as a string or number). + * - `contractCall`: Defines the contract call that will be executed on the destination chain: + * - For native contract calls (`NativeContractCall`): + * - `callData`: The encoded data to be sent to the contract. + * - `contractAddress`: The address of the contract to call. + * - `gasLimit`: The gas limit for the contract call. + * - `recipient`: The address of the recipient of the call. + * - For token contract calls (`TokenContractCall`): + * - `callData`: The encoded data to be sent to the contract. + * - `contractAddress`: The address of the contract to call. + * - `gasLimit`: The gas limit for the contract call. + * - `outputTokenAddress` (optional): The address of the output token (if different from the input). + * - `approvalAddress` (optional): The address to approve for spending tokens. + * - `sourceChains` (optional): An array of whitelisted source chain IDs for the transfer. + * - `threshold` (optional): The minimum amount threshold required for bridging. + * + * @param {FetchOptions} [options] - Optional configuration for the fetch request, such as custom headers or query parameters. + * + * @returns {Promise} A promise that resolves to the solution object containing the optimal bridging strategy and contract call, or a `FailedSolution` in case of an error. + * + * @note This function is a work in progress and will be implemented in the future. + * + * @example + * ```ts + * import { Sprinter } from '@chainsafe/sprinter-sdk'; + * + * const sprinter = new Sprinter(); + * + * const settings = { + * account: "0x3e101ec02e7a48d16dade204c96bff842e7e2519", + * destinationChain: 11155111, + * token: "USDC", + * amount: "100000000", + * contractCall: { + * callData: "0xabcdef", // encoded contract call data + * contractAddress: "0x1234567890abcdef", + * gasLimit: 21000, + * recipient: "0xRecipientAddress" // for native contract call + * }, + * sourceChains: [84532, 137] + * }; + * + * sprinter.bridgeAggregateBalanceAndCall(settings).then(solution => { + * console.log(solution); + * }).catch(error => { + * console.error(error); + * }); + * ``` + */ public async bridgeAggregateBalanceAndCall( settings: Infer, options?: FetchOptions, ): Promise { + throw new Error("TODO"); + assert(settings, MultiHopWithContractSchema); const { sourceChains, amount, ...data } = settings; @@ -111,35 +346,133 @@ export class Sprinter { ); } + /** + * Fetches and returns the optimal bridging solution for transferring tokens between two blockchains using a single-hop strategy. + * + * This method finds the best path for transferring tokens from a source chain to a destination chain, as specified by the provided settings. + * + * @param {Infer} settings - The settings object for defining the bridging parameters: + * - `account`: The user's wallet address for the transaction. + * - `destinationChain`: The ID of the destination blockchain. + * - `token`: The token symbol (e.g., "ETH", "USDC") to be bridged. + * - `amount`: The amount of tokens to bridge (as a string or number). + * - `threshold` (optional): The minimum amount threshold required for bridging. + * - `sourceChain` (optional): The source chain ID for the transfer. If not provided, the best source chain will be determined. + * + * @param {FetchOptions} [options] - Optional configuration for the fetch request, such as custom headers or query parameters. + * + * @returns {Promise} A promise that resolves to the solution object containing the optimal bridging strategy, or a `FailedSolution` in case of an error. + * + * @example + * ```ts + * import { Sprinter } from '@chainsafe/sprinter-sdk'; + * + * const sprinter = new Sprinter(); + * + * const settings = { + * account: "0x3e101ec02e7a48d16dade204c96bff842e7e2519", + * destinationChain: 11155111, + * token: "USDC", + * amount: "100000000", + * sourceChain: 84532 // optional + * }; + * + * sprinter.bridge(settings).then(solution => { + * console.log(solution); + * }).catch(error => { + * console.error(error); + * }); + * ``` + * + * @note If the `sourceChain` is not provided, the method will determine the best source chain for the transfer. + */ public async bridge( settings: Infer, options?: FetchOptions, ): Promise { assert(settings, SingleHopSchema); - const { sourceChains, amount, ...data } = settings; + const { sourceChain, amount, ...data } = settings; return await getContractCallSolution( { ...data, amount: BigInt(amount), - whitelistedSourceChains: sourceChains ? [sourceChains] : [], + whitelistedSourceChains: sourceChain ? [sourceChain] : [], } as SolutionOptions, options, ); } + /** + * Fetches and returns the optimal bridging solution and performs a contract call on the destination chain using a single-hop strategy. + * + * This method transfers tokens from a source chain to a destination chain and then executes a contract call on the destination chain. + * + * @param {Infer} settings - The settings object for defining the bridging and contract call parameters: + * - `account`: The user's wallet address for the transaction. + * - `destinationChain`: The ID of the destination blockchain. + * - `token`: The token symbol (e.g., "ETH", "USDC") to be bridged. + * - `amount`: The amount of tokens to bridge (as a string or number). + * - `contractCall`: Defines the contract call that will be executed on the destination chain: + * - For native contract calls (`NativeContractCall`): + * - `callData`: The encoded data to be sent to the contract. + * - `contractAddress`: The address of the contract to call. + * - `gasLimit`: The gas limit for the contract call. + * - `recipient`: The address of the recipient of the call. + * - For token contract calls (`TokenContractCall`): + * - `callData`: The encoded data to be sent to the contract. + * - `contractAddress`: The address of the contract to call. + * - `gasLimit`: The gas limit for the contract call. + * - `outputTokenAddress` (optional): The address of the output token (if different from the input). + * - `approvalAddress` (optional): The address to approve for spending tokens. + * - `sourceChain` (optional): The source chain ID for the transfer. If not provided, the best source chain will be determined. + * - `threshold` (optional): The minimum amount threshold required for bridging. + * + * @param {FetchOptions} [options] - Optional configuration for the fetch request, such as custom headers or query parameters. + * + * @returns {Promise} A promise that resolves to the solution object containing the optimal bridging strategy and contract call, or a `FailedSolution` in case of an error. + * + * @example + * ```ts + * import { Sprinter } from '@chainsafe/sprinter-sdk'; + * + * const sprinter = new Sprinter(); + * + * const settings = { + * account: "0x3e101ec02e7a48d16dade204c96bff842e7e2519", + * destinationChain: 11155111, + * token: "USDC", + * amount: "100000000", + * contractCall: { + * callData: "0xabcdef", // encoded contract call data + * contractAddress: "0x1234567890abcdef", + * gasLimit: 21000, + * recipient: "0xRecipientAddress" // for native contract call + * }, + * sourceChain: 84532 // optional + * }; + * + * sprinter.bridgeAndCall(settings).then(solution => { + * console.log(solution); + * }).catch(error => { + * console.error(error); + * }); + * ``` + * + * @note If the `sourceChain` is not provided, the method will determine the best source chain for the transfer. + */ public async bridgeAndCall( settings: Infer, options?: FetchOptions, ): Promise { assert(settings, SingleHopWithContractSchema); - const { sourceChains, amount, ...data } = settings; + const { sourceChain, amount, ...data } = settings; return await getContractCallSolution( { ...data, amount: BigInt(amount), - whitelistedSourceChains: sourceChains ? [sourceChains] : [], + whitelistedSourceChains: sourceChain ? [sourceChain] : [], } as SolutionOptions, options, ); From c72bc783fe2e5683bca6cb866bfccc044b64e84f Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Mon, 30 Sep 2024 16:04:02 +0200 Subject: [PATCH 08/44] add enum :man_shrugging: --- packages/sdk/src/enums.ts | 5 +++++ packages/sdk/src/index.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/enums.ts b/packages/sdk/src/enums.ts index 7b0fa3e..c6143f9 100644 --- a/packages/sdk/src/enums.ts +++ b/packages/sdk/src/enums.ts @@ -1,3 +1,8 @@ export enum ChainType { EVM = "evm", } + +export enum Environment { + MAINNET = "https://api.sprinter.buildwithsygma.com/", + TESTNET = "https://api.test.sprinter.buildwithsygma.com/", +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 3a0ea35..881b5ae 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,4 +1,4 @@ export type * from "./types"; export * as api from "./api"; -export * from "./enums"; +export { ChainType, Environment } from "./enums"; export { Sprinter } from "./sprinter"; From ddd45f2739d6d18bacd8d296b844f73a8d99357a Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Mon, 30 Sep 2024 16:31:40 +0200 Subject: [PATCH 09/44] micro bug fixes --- packages/sdk/src/index.ts | 1 + packages/sdk/src/internal/validators.ts | 2 +- packages/sdk/src/sprinter.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 881b5ae..2dea3a5 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,4 +1,5 @@ export type * from "./types"; +export { setBaseUrl, BASE_URL } from "./api"; export * as api from "./api"; export { ChainType, Environment } from "./enums"; export { Sprinter } from "./sprinter"; diff --git a/packages/sdk/src/internal/validators.ts b/packages/sdk/src/internal/validators.ts index f394fc6..104f4e8 100644 --- a/packages/sdk/src/internal/validators.ts +++ b/packages/sdk/src/internal/validators.ts @@ -20,7 +20,7 @@ const hexString = (): Struct => }); const numberLike = refine( - union([number(), hexString(), bigint()]), + union([number(), string(), hexString(), bigint()]), "numberLike", (value) => { if (typeof value === "string") return !isNaN(Number(value)); diff --git a/packages/sdk/src/sprinter.ts b/packages/sdk/src/sprinter.ts index 4da49fd..437ad6f 100644 --- a/packages/sdk/src/sprinter.ts +++ b/packages/sdk/src/sprinter.ts @@ -462,7 +462,7 @@ export class Sprinter { * @note If the `sourceChain` is not provided, the method will determine the best source chain for the transfer. */ public async bridgeAndCall( - settings: Infer, + settings: Infer, options?: FetchOptions, ): Promise { assert(settings, SingleHopWithContractSchema); From c61895fe7e7da0637ddc22a7048851c3a5db2976 Mon Sep 17 00:00:00 2001 From: BeroBurny Date: Mon, 30 Sep 2024 16:31:49 +0200 Subject: [PATCH 10/44] refactor POC to match new SDK --- web/src/lib/components/SendTokensDrawer.svelte | 8 ++++---- web/src/lib/components/SubmitTokensDrawer.svelte | 4 ++-- web/src/lib/components/UpdateNameModal.svelte | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/lib/components/SendTokensDrawer.svelte b/web/src/lib/components/SendTokensDrawer.svelte index 7445b2f..9c933a5 100644 --- a/web/src/lib/components/SendTokensDrawer.svelte +++ b/web/src/lib/components/SendTokensDrawer.svelte @@ -18,7 +18,7 @@ import type { Address } from '@chainsafe/sprinter-sdk'; import { formatWei } from '$lib/formatters'; - const tokens = $sprinter.getAvailableTokens($selectedAccount as Address).then((tokens) => [ + const tokens = $sprinter.getAvailableTokens().then((tokens) => [ ...tokens, { addresses: [], @@ -28,7 +28,7 @@ symbol: 'ETH' } ]); - const allBalances = $sprinter.getUserBalances(); + const allBalances = $sprinter.getUserBalances($selectedAccount as Address); const chains = $sprinter.getAvailableChains(); const drawerStore = getDrawerStore(); @@ -81,8 +81,8 @@ quota: { account: $selectedAccount, token: selectedToken, - destinationChain: selectedNetwork, - whitelisted, + destinationChain: Number(selectedNetwork), + sourceChains: whitelisted.map(Number), amount: toWei(amount, tokenInfo.decimals), threshold: threshold ? toWei(threshold, tokenInfo.decimals) : undefined } diff --git a/web/src/lib/components/SubmitTokensDrawer.svelte b/web/src/lib/components/SubmitTokensDrawer.svelte index ae83354..e2f15a9 100644 --- a/web/src/lib/components/SubmitTokensDrawer.svelte +++ b/web/src/lib/components/SubmitTokensDrawer.svelte @@ -7,7 +7,7 @@ import { formatWei } from '$lib/formatters'; const drawerStore = getDrawerStore(); - $: quota = $sprinter.getSolution($drawerStore.meta.quota); + $: quota = $sprinter.bridgeAggregateBalance($drawerStore.meta.quota); $: token = getTokenBySymbol($drawerStore.meta.tokens, $drawerStore.meta.quota.token); @@ -63,7 +63,7 @@ >