From c42ca11c119c7322d80d48bdcc6b236c825655d1 Mon Sep 17 00:00:00 2001 From: MarkacRobi Date: Thu, 14 Nov 2024 18:56:59 +0100 Subject: [PATCH 1/2] chore: docs and other improvements --- README.md | 236 ++++++++++++++++++++++++++++++- src/services/IntentService.ts | 42 ++++-- src/services/SolverApiService.ts | 52 +++++-- src/types.ts | 17 ++- 4 files changed, 324 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index eb97a34..910dd09 100644 --- a/README.md +++ b/README.md @@ -1 +1,235 @@ -# balanced-solver-sdk +# Balanced Intent Solver SDK (Alpha) + +Balanced Intent Solver SDK provides abstractions to assist you with interacting with the cross-chain Intent Smart Contracts and Solver. + +**Note** SDK is currently in alpha testing stage and is subject to change! + +## Installation + +### NPM + +Installing through npm: + +`npm i --save @balanced/solver-sdk` + +**NOTE** Package is not yet published to the npm registry!!! + +### Local + +Package can be locally installed by following this steps: + +1. Clone this repository to your local machine. +2. `cd` into repository folder location. +3. Execute `npm install` command in your CLI to install dependencies. +4. Execute `npm run build` to build the package. +5. In your app repository `package.json` file, define dependency named `"@balanced/solver-sdk"` under `"dependencies"`. + Instead of version define absolute path to your SDK repository `"file:"` (e.g. `"file:/Users/dev/balanced-solver-sdk"`). + Full example: `"@balanced/solver-sdk": "file:/Users/dev/balanced-solver-sdk"`. + +## Local Development + +How to setup local development + +1. Clone repository. +2. Make sure you have [Node.js](https://nodejs.org/en/download/package-manager) v18+ and corresponding npm installed on your system. +3. Execute `npm install` command in your CLI to install dependencies. +4. Make code changes. + 1. Do not forget to export TS files in same folder `index.ts`. + 2. Always import files using `.js` postfix. +5. Before commiting execute `npm run prepublishOnly` in order to verify build, format and exports. + +## Load SDK Config + +SDK includes predefined configurations of supported chains, tokens and other relevant information for the client to consume. + +```typescript +import { ChainName, ChainConfig, chainConfig, Token } from "@balanced/solver-sdk" + +// all supported Intent chains +const supportedChains: ChainName[] = IntentService.getSupportedChains() + +// retrieve arbitrum chain config +const arbChainConfig: EvmChainConfig = IntentService.getChainConfig("arb") + +// example of how to construct token per chain map using supported chains array +const supportedTokensPerChain: Map = new Map( + supportedChains.map((chain) => { + return [chain, IntentService.getChainConfig(chain).supportedTokens] + }), +) + +// example of how to construct chain name to chain config map +const chainConfigs: Map = new Map( + supportedChains.map((chain) => { + return [chain, IntentService.getChainConfig(chain)] + }), +) +``` + +## Request a Quote + +Requesting a quote should require you to just consume user input amount and converting it to the appropriate token amount (scaled by token decimals). +All the required configurations (chain id [nid], token decimals and address) should be loaded as described in [Load SDK Config](#load-sdk-config). + +```typescript +import { IntentService } from "@balanced/solver-sdk" + +const quoteResult = await IntentService.getQuote({ + token_src: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + token_src_blockchain_id: "0xa4b1.arbitrum", + token_dst: "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI", + token_dst_blockchain_id: "sui", + src_amount: "100000000000000000", +}) + +/** + * Example response (quoteResult) + * { + * "ok": true, + * "value": { + * "output": { + * "expected_output":"1000000000000", // to be used in create intent order as toAmount + * "uuid":"e2795d2c-14a5-4d18-9be6-a257d7c9d274" // to be used in create intent order as quote_uuid + * } + * } + * } + */ +``` + +## Initialising Providers + +SDK abstracts away the wallet and public RPC clients using `ChainProviderType` TS type which can be one of the following: + +- `EvmProvider`: Provider used for EVM type chains (ETH, BSC, etc..). Implemented using [viem](https://viem.sh/docs/clients/wallet#json-rpc-accounts). +- `SuiProvider`: Provider used for SUI type chains (SUI). Implemented using [@mysten/sui](https://github.com/MystenLabs/sui) and [@mysten/wallet-standard](https://docs.sui.io/standards/wallet-standard). + +Providers are used to request wallet actions (prompts wallet extension) and make RPC calls to the RPC nodes. + +EVM Provider example: + +```typescript +import { EvmProvider } from "@balanced/solver-sdk" + +// NOTE: user address should be provided by application when user connects wallet +const evmProvider = new EvmProvider("0x3FF796F1968C515f6AC2833545B5Dd2cE765A1a1", (window as any).ethereum) +``` + +SUI Provider example (uses [SUI dApp Kit](https://sdk.mystenlabs.com/dapp-kit/): + +```typescript +import { SuiProvider } from "@balanced/solver-sdk" +import { useCurrentWallet, useCurrentAccount } from "@mysten/dapp-kit" + +const account = useCurrentAccount() +const { currentWallet, connectionStatus } = useCurrentWallet() + +// check that wallet is connected and account is defined +if (connectionStatus === "connected" && account) { + const suiProvider = new SuiProvider(currentWallet, account, "mainnet") +} else { + throw new Error("Wallet or Account undefined. Please connect wallet and select account.") +} +``` + +## Create Intent Order + +Creating Intent Order requires creating provider for the chain that intent is going to be created on (`fromChain`). + +Example for ARB -> SUI Intent Order: + +```typescript +import { IntentService, EvmProvider, CreateIntentOrderPayload, IntentStatusCode } from "@balanced/solver-sdk" + +// create EVM provider because "arb" is of ChainType "evm" (defined in ChainConfig type - see section Load SDK Config) +// NOTE: window can only be accessed client side (browser) +const evmProvider = new EvmProvider("0x601020c5797Cdd34f64476b9bf887a353150Cb9a", (window as any).ethereum) + +const intentOrderPayload: CreateIntentOrderPayload = { + quote_uuid: "a0dd7652-b360-4123-ab2d-78cfbcd20c6b", + fromAddress: "0x601020c5797Cdd34f64476b9bf887a353150Cb9a", // address we are sending funds from (fromChain) + toAddress: "0x81600ec58a2efd97f41380370cddf25b7a416d03ee081552becfa9710ea30878", // destination address where funds are transfered to (toChain) + fromChain: "arb", // ChainName + toChain: "sui", // ChainName + token: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + amount: BigInt("100000000000000000"), + toToken: "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI", + toAmount: BigInt("1000000000000"), +} as const + +// checks if token transfer amount is approved (required for EVM, can be skipped for SUI as it defaults to true) +const isAllowanceValid = await IntentService.isAllowanceValid(intentOrderPayload, evmProvider) + +if (isAllowanceValid.ok) { + if (!isAllowanceValid.value) { + // allowance invalid, prompt approval + const approvalResult = await IntentService.approve( + "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + BigInt("100000000000000000"), + IntentService.getChainConfig("arb").intentContract, + evmProvider, + ) + + if (approvalResult.ok) { + const executionResult = await IntentService.executeIntentOrder(intentOrderPayload, evmProvider) + + if (executionResult.ok) { + const intentStatus = await IntentService.getStatus({ + task_id: executionResult.value.task_id, + }) + + if (intentStatus.ok) { + console.log(intentStatus) + + /** + * Example status + * { + * "ok": true, + * "value": { + * "output": { + * "status":3, // use IntentStatusCode to map status code + * "tx_hash":"0xabcdefasdasdsafssadasdsadsadasdsadasdsadsa" + * } + * } + * } + */ + } else { + // handle error + } + } else { + // handle error + } + } else { + // handle error + } + } +} else { + // handle error +} +``` + +## Get Intent Status + +After Intent Order is created, the resulting `task_id` can be used to query the status of the task. + +Example status check: + +```typescript +import { IntentService } from "@balanced/solver-sdk" + +const intentStatus = await IntentService.getStatus({ + task_id: "a0dd7652-b360-4123-ab2d-78cfbcd20c6b", +}) + + /** + * Example intentStatus response + * { + * "ok": true, + * "value": { + * "output": { + * "status":3, // use IntentStatusCode to map status code + * "tx_hash":"0xabcdefasdasdsafssadasdsadsadasdsadasdsadsa" + * } + * } + * } + */ +``` diff --git a/src/services/IntentService.ts b/src/services/IntentService.ts index 0b883ea..96eca6f 100644 --- a/src/services/IntentService.ts +++ b/src/services/IntentService.ts @@ -1,7 +1,6 @@ -import { type Address, type Hash } from "viem" +import { type Address, type Hash, type TransactionReceipt } from "viem" import { type ChainName, - type ChainConfig, type Result, type IntentQuoteRequest, type IntentQuoteResponse, @@ -10,6 +9,7 @@ import { type IntentExecutionResponse, type IntentStatusRequest, type IntentStatusResponse, + type GetChainConfigType, } from "../types.js" import { chainConfig, supportedChains } from "../constants.js" import { isEvmChainConfig, isSuiChainConfig } from "../guards.js" @@ -31,7 +31,7 @@ export class IntentService { * "token_src_blockchain_id": "42161", * "token_dst": "0x2::sui::SUI", * "token_dst_blockchain_id": "101", - * "src_amount": "0.00001" + * "src_amount": "10000" * } * * // response @@ -39,7 +39,7 @@ export class IntentService { * "ok": true, * "value": { * "output": { - * "expected_output":"0.009813013", + * "expected_output":"981301300", * "uuid":"e2795d2c-14a5-4d18-9be6-a257d7c9d274" * } * } @@ -60,7 +60,6 @@ export class IntentService { provider: GetChainProviderType, ): Promise> { invariant(payload.amount > 0n, "Invalid amount") - invariant(payload.amount > 0n, "Invalid amount") try { const fromChainConfig = chainConfig[payload.fromChain] @@ -107,13 +106,36 @@ export class IntentService { } } + /** + * Approve ERC20 amount spending + * @param token - ERC20 token address + * @param amount - Amount to approve + * @param address - Address to approve spending for + * @param provider - EVM Provider + */ + static async approve( + token: Address, + amount: bigint, + address: Address, + provider: EvmProvider, + ): Promise> { + return EvmIntentService.approve(token, amount, address, provider) + } + /** * Execute intent order * @example * // request * { - * "intent_tx_hash": "0xba3dce19347264db32ced212ff1a2036f20d9d2c7493d06af15027970be061af", - * "quote_uuid": "a0dd7652-b360-4123-ab2d-78cfbcd20c6b" + * "quote_uuid": "a0dd7652-b360-4123-ab2d-78cfbcd20c6b", + * "fromAddress": "0x601020c5797Cdd34f64476b9bf887a353150Cb9a", + * "toAddress": "0x81600ec58a2efd97f41380370cddf25b7a416d03ee081552becfa9710ea30878", + * "fromChain": "0xa4b1.arbitrum", + * "toChain": "sui", + * "token": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + * "amount": "10000", + * "toToken": "0x2::sui::SUI", + * "toAmount": "9813013000", * } * * // response @@ -219,7 +241,7 @@ export class IntentService { * "value": { * "output": { * "status":3, - * "tx_hash":"0xabcdef" + * "tx_hash":"0xabcdefasdasdsafssadasdsadsadasdsadasdsadsa" * } * } * } @@ -240,13 +262,13 @@ export class IntentService { /** * Get config of a specific chain */ - public static getChainConfig(chain: ChainName): ChainConfig { + public static getChainConfig(chain: T): GetChainConfigType { const data = chainConfig[chain] if (!chainConfig) { throw new Error(`Unsupported chain: ${chain}`) } - return data + return data as GetChainConfigType } } diff --git a/src/services/SolverApiService.ts b/src/services/SolverApiService.ts index 8339568..9a585a3 100644 --- a/src/services/SolverApiService.ts +++ b/src/services/SolverApiService.ts @@ -9,20 +9,19 @@ import { type IntentStatusRequest, type IntentStatusResponse, type Result, + type IntentQuoteResponseRaw, } from "../types.js" import invariant from "tiny-invariant" export class SolverApiService { private constructor() {} - public static async getQuote( - intentQuoteRequest: IntentQuoteRequest, - ): Promise> { - invariant(intentQuoteRequest.token_src.length > 0, "Empty token_src") - invariant(intentQuoteRequest.token_src_blockchain_id.length > 0, "Empty token_src_blockchain_id") - invariant(intentQuoteRequest.token_dst.length > 0, "Empty token_dst") - invariant(intentQuoteRequest.token_dst_blockchain_id.length > 0, "Empty token_dst_blockchain_id") - invariant(+intentQuoteRequest.src_amount > 0, "Invalid src_amount amount") + public static async getQuote(payload: IntentQuoteRequest): Promise> { + invariant(payload.token_src.length > 0, "Empty token_src") + invariant(payload.token_src_blockchain_id.length > 0, "Empty token_src_blockchain_id") + invariant(payload.token_dst.length > 0, "Empty token_dst") + invariant(payload.token_dst_blockchain_id.length > 0, "Empty token_dst_blockchain_id") + invariant(payload.src_amount > 0n, "src_amount must be greater than 0") try { const response = await fetch(`${SOLVER_API_ENDPOINT}/quote`, { @@ -30,7 +29,13 @@ export class SolverApiService { headers: { "Content-Type": "application/json", }, - body: JSON.stringify(intentQuoteRequest), + body: JSON.stringify({ + token_src: payload.token_src, + token_src_blockchain_id: payload.token_src_blockchain_id, + token_dst: payload.token_dst, + token_dst_blockchain_id: payload.token_dst_blockchain_id, + src_amount: payload.src_amount.toString(), + }), }) if (!response.ok) { @@ -40,9 +45,16 @@ export class SolverApiService { } } + const quoteResponse: IntentQuoteResponseRaw = await response.json() + return { ok: true, - value: await response.json(), + value: { + output: { + expected_output: BigInt(quoteResponse.output.expected_output), + uuid: quoteResponse.output.uuid, + }, + } satisfies IntentQuoteResponse, } } catch (e: any) { console.error(`[SolverApiService.getQuote] failed. Details: ${JSON.stringify(e)}`) @@ -58,6 +70,26 @@ export class SolverApiService { } } + /** + * Execute intent order + * @example + * // request + * { + * "intent_tx_hash": "0xba3dce19347264db32ced212ff1a2036f20d9d2c7493d06af15027970be061af", + * "quote_uuid": "a0dd7652-b360-4123-ab2d-78cfbcd20c6b" + * } + * + * // response + * { + * "ok": true, + * "value": { + * "output": { + * "answer":"OK", + * "task_id":"a0dd7652-b360-4123-ab2d-78cfbcd20c6b" + * } + * } + * } + */ public static async postExecution( intentExecutionRequest: IntentExecutionRequest, ): Promise> { diff --git a/src/types.ts b/src/types.ts index c7c8db3..f8e5234 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,6 +49,12 @@ export type SuiChainConfig = BaseChainConfig<"sui"> & { export type ChainConfig = EvmChainConfig | SuiChainConfig +export type GetChainConfigType = T extends "arb" + ? EvmChainConfig + : T extends "sui" + ? SuiChainConfig + : never + export type Result = { ok: true; value: T } | { ok: false; error: E } export type SuiNetworkType = "mainnet" | "testnet" | "devnet" | "localnet" @@ -58,12 +64,19 @@ export type IntentQuoteRequest = { token_src_blockchain_id: string token_dst: string token_dst_blockchain_id: string - src_amount: string // normalised decimal amount (NOT token amount scaled by decimals!!!) + src_amount: bigint +} + +export type IntentQuoteResponseRaw = { + output: { + expected_output: string + uuid: string + } } export type IntentQuoteResponse = { output: { - expected_output: string // normalised decimal amount (NOT token amount scaled by decimals!!!) + expected_output: bigint uuid: string } } From 837e41d3db10a7cf20089066552b268b1a777971 Mon Sep 17 00:00:00 2001 From: MarkacRobi Date: Thu, 14 Nov 2024 19:01:10 +0100 Subject: [PATCH 2/2] fix: format --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 910dd09..337ef09 100644 --- a/README.md +++ b/README.md @@ -217,19 +217,19 @@ Example status check: import { IntentService } from "@balanced/solver-sdk" const intentStatus = await IntentService.getStatus({ - task_id: "a0dd7652-b360-4123-ab2d-78cfbcd20c6b", + task_id: "a0dd7652-b360-4123-ab2d-78cfbcd20c6b", }) - /** - * Example intentStatus response - * { - * "ok": true, - * "value": { - * "output": { - * "status":3, // use IntentStatusCode to map status code - * "tx_hash":"0xabcdefasdasdsafssadasdsadsadasdsadasdsadsa" - * } - * } - * } - */ +/** + * Example intentStatus response + * { + * "ok": true, + * "value": { + * "output": { + * "status":3, // use IntentStatusCode to map status code + * "tx_hash":"0xabcdefasdasdsafssadasdsadsadasdsadasdsadsa" + * } + * } + * } + */ ```