From e8b88bd31637ec1f2b4c524fc36e5c040623eb0e Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Tue, 15 Oct 2024 12:38:02 +0200 Subject: [PATCH 01/53] queries --- eslint.config.js | 7 + package.json | 2 - src/Centrifuge.ts | 143 +++++++++++++++++- src/Pool.ts | 44 ++++++ src/PoolDomain.ts | 67 ++++++++ .../liquidityPools/CentrifugeRouter.abi.json | 5 + src/abi/liquidityPools/Currency.abi.json | 1 + src/abi/liquidityPools/Gateway.abi.json | 1 + .../liquidityPools/InvestmentManager.abi.json | 1 + src/abi/liquidityPools/LiquidityPool.abi.json | 15 ++ src/abi/liquidityPools/Permit.abi.json | 1 + src/abi/liquidityPools/PoolManager.abi.json | 5 + src/abi/liquidityPools/Router.abi.json | 1 + src/abi/liquidityPools/index.ts | 20 +++ src/config/chains.ts | 3 + src/config/lp.ts | 32 ++++ src/constants.ts | 1 + src/types/index.ts | 1 + src/types/query.ts | 12 ++ src/utils/pinToApi.ts | 9 ++ tsconfig.json | 2 +- yarn.lock | 64 -------- 22 files changed, 369 insertions(+), 68 deletions(-) create mode 100644 src/Pool.ts create mode 100644 src/PoolDomain.ts create mode 100644 src/abi/liquidityPools/CentrifugeRouter.abi.json create mode 100644 src/abi/liquidityPools/Currency.abi.json create mode 100644 src/abi/liquidityPools/Gateway.abi.json create mode 100644 src/abi/liquidityPools/InvestmentManager.abi.json create mode 100644 src/abi/liquidityPools/LiquidityPool.abi.json create mode 100644 src/abi/liquidityPools/Permit.abi.json create mode 100644 src/abi/liquidityPools/PoolManager.abi.json create mode 100644 src/abi/liquidityPools/Router.abi.json create mode 100644 src/abi/liquidityPools/index.ts create mode 100644 src/config/chains.ts create mode 100644 src/config/lp.ts create mode 100644 src/constants.ts create mode 100644 src/types/index.ts create mode 100644 src/types/query.ts create mode 100644 src/utils/pinToApi.ts diff --git a/eslint.config.js b/eslint.config.js index c621a7c..f80a934 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,13 @@ export default [ 'object-shorthand': 'error', 'prefer-template': 'error', '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'all', + argsIgnorePattern: '^_', + }, + ], }, }, ] diff --git a/package.json b/package.json index 352e8fc..6ff85d4 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,9 @@ "prepare": "yarn build" }, "dependencies": { - "isomorphic-fetch": "^3.0.0", "rxjs": "^7.8.1" }, "devDependencies": { - "@types/isomorphic-fetch": "^0.0.36", "eslint": "^9.12.0", "globals": "^15.11.0", "npm-run-all": "4.1.5", diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 89e5503..bf1001b 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -1 +1,142 @@ -export class Centrifuge {} +import type { MonoTypeOperatorFunction, Observable } from 'rxjs' +import { firstValueFrom, map, of, ReplaySubject, share, timer } from 'rxjs' +import { createClient, http, type Client } from 'viem' +import { chains } from './config/chains.js' +import { Pool } from './Pool.js' +import type { CentrifugeQueryOptions, Query } from './types/query.js' +import { pinToApi } from './utils/pinToApi.js' + +export type Config = { + environment: 'mainnet' | 'demo' | 'dev' + subqueryUrl: string + ipfsHost: string + pinFile: (b64URI: string) => Promise<{ + uri: string + }> + pinJson: (json: string) => Promise<{ + uri: string + }> +} +export type UserProvidedConfig = Partial + +const envConfig = { + mainnet: { + subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', + pinningApiUrl: 'https://europe-central2-centrifuge-production-x.cloudfunctions.net/pinning-api-production', + alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', + infuraKey: '8ed99a9a115349bbbc01dcf3a24edc96', + }, + demo: { + subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', + pinningApiUrl: 'https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-demo', + alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', + infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', + }, + dev: { + subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', + pinningApiUrl: 'https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-demo', + alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', + infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', + }, +} + +const defaultConfig = { + environment: 'mainnet', + ipfsHost: 'https://metadata.centrifuge.io', +} satisfies UserProvidedConfig + +export class Centrifuge { + #config: Config + get config() { + return this.#config + } + + #clients = new Map() + getClient(chainId: number) { + return this.#clients.get(chainId) + } + get chains() { + return [...this.#clients.keys()] + } + + constructor(config: UserProvidedConfig = {}) { + const defaultConfigForEnv = envConfig[config?.environment ?? 'mainnet'] + this.#config = { + ...defaultConfig, + subqueryUrl: defaultConfigForEnv.subqueryUrl, + pinFile: (b64URI) => + pinToApi('pinFile', defaultConfigForEnv.pinningApiUrl, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ uri: b64URI }), + }), + pinJson: (json) => + pinToApi('pinJson', defaultConfigForEnv.pinningApiUrl, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ json }), + }), + ...config, + } + Object.freeze(this.#config) + chains + .filter((chain) => (this.#config.environment === 'mainnet' ? !chain.testnet : chain.testnet)) + .forEach((chain) => { + this.#clients.set(chain.id, createClient({ chain, transport: http(), batch: { multicall: true } })) + }) + } + + pool(id: string) { + return this._query(null, () => of(new Pool(this, id))) + } + + #memoized = new Map() + _memoizeWith(keys: (string | number)[], cb: () => T): T { + const cacheKey = JSON.stringify(keys) + + if (this.#memoized.has(cacheKey)) { + return this.#memoized.get(cacheKey) + } + + const result = cb() + this.#memoized.set(cacheKey, result) + + return result + } + + _query(keys: (string | number)[] | null, cb: () => Observable, options?: CentrifugeQueryOptions): Query { + function get() { + const $ = cb().pipe( + keys + ? shareReplayWithDelayedReset({ bufferSize: 1, resetDelay: options?.cacheTime ?? 20000 }) + : map((val) => val) + ) + const obj: Query = Object.assign($, { + then(onfulfilled: (value: T) => any, onrejected: (reason: any) => any) { + return firstValueFrom($).then(onfulfilled, onrejected) + }, + }) + return obj + } + return keys ? this._memoizeWith(keys, get) : get() + } + + _makeQuery(baseKeys: (string | number)[]) { + return (keys: (string | number)[] | null, cb: () => Observable, options?: CentrifugeQueryOptions) => + this._query(keys ? [...baseKeys, ...keys] : null, cb, options) + } +} + +export function shareReplayWithDelayedReset(config?: { + bufferSize?: number + windowTime?: number + resetDelay?: number +}): MonoTypeOperatorFunction { + const { bufferSize = Infinity, windowTime = Infinity, resetDelay = 1000 } = config ?? {} + return share({ + connector: () => new ReplaySubject(bufferSize, windowTime), + resetOnError: true, + resetOnComplete: false, + resetOnRefCountZero: isFinite(resetDelay) ? () => timer(resetDelay) : false, + }) +} diff --git a/src/Pool.ts b/src/Pool.ts new file mode 100644 index 0000000..784fa4c --- /dev/null +++ b/src/Pool.ts @@ -0,0 +1,44 @@ +import { catchError, combineLatest, map, of, switchMap, timeout } from 'rxjs' +import type { Centrifuge } from './Centrifuge.js' +import { PoolDomain } from './PoolDomain.js' +import type { QueryFn } from './types/query.js' + +export class Pool { + private _query: QueryFn + constructor(private _root: Centrifuge, public id: string) { + this._query = this._root._makeQuery(['pool', this.id]) + } + + domains() { + return this._query(null, () => { + return of( + this._root.chains.map((chainId) => { + return new PoolDomain(this._root, this, chainId) + }) + ) + }) + } + + activeDomains() { + return this._query(null, () => { + return this.domains().pipe( + switchMap((domains) => { + return combineLatest( + domains.map((domain) => + domain.isActive().pipe( + timeout(8000), + catchError(() => { + return of(false) + }) + ) + ) + ).pipe( + map((isActive) => { + return domains.filter((_, index) => isActive[index]) + }) + ) + }) + ) + }) + } +} diff --git a/src/PoolDomain.ts b/src/PoolDomain.ts new file mode 100644 index 0000000..d30e415 --- /dev/null +++ b/src/PoolDomain.ts @@ -0,0 +1,67 @@ +import { defer, switchMap, tap } from 'rxjs' +import { getContract } from 'viem' +import { ABI } from './abi/liquidityPools/index.js' +import type { Centrifuge } from './Centrifuge.js' +import { lpConfig } from './config/lp.js' +import type { Pool } from './Pool.js' +import type { HexString } from './types/index.js' +import type { QueryFn } from './types/query.js' + +export class PoolDomain { + private _query: QueryFn + constructor(private _root: Centrifuge, public pool: Pool, public chainId: number) { + this._query = this._root._makeQuery(['pool', this.pool.id, 'domain', this.chainId]) + } + + manager() { + return this._root._query( + ['domainManager', this.chainId], + () => + defer(async () => { + const { router } = lpConfig[this.chainId]! + const client = this._root.getClient(this.chainId)! + const gatewayAddress = await getContract({ address: router, abi: ABI.Router, client }).read.gateway!() + const managerAddress = await getContract({ address: gatewayAddress as any, abi: ABI.Gateway, client }).read + .investmentManager!() + console.log('managerAddress', managerAddress) + return managerAddress as HexString + }), + { cacheTime: Infinity } + ) + } + + poolManager() { + return this._root._query( + ['domainPoolManager', this.chainId], + () => + this.manager().pipe( + switchMap((manager) => { + return getContract({ + address: manager, + abi: ABI.InvestmentManager, + client: this._root.getClient(this.chainId)!, + }).read.poolManager!() as Promise + }), + tap((poolManager) => console.log('poolManager', poolManager)) + ), + { cacheTime: Infinity } + ) + } + + isActive() { + return this._query( + ['isActive'], + () => + this.poolManager().pipe( + switchMap((manager) => { + return getContract({ + address: manager, + abi: ABI.PoolManager, + client: this._root.getClient(this.chainId)!, + }).read.isPoolActive!([this.pool.id]) as Promise + }) + ), + { cacheTime: Infinity } + ) + } +} diff --git a/src/abi/liquidityPools/CentrifugeRouter.abi.json b/src/abi/liquidityPools/CentrifugeRouter.abi.json new file mode 100644 index 0000000..e721d35 --- /dev/null +++ b/src/abi/liquidityPools/CentrifugeRouter.abi.json @@ -0,0 +1,5 @@ +[ + "function estimate(bytes) view returns (uint256)", + "function _transferTrancheTokens(address,uint8,uint64,address,uint128,uint256) external payable returns (bool)", + "function transferTrancheTokens(address,uint8,uint64,bytes32,uint128,uint256) public payable returns (bool)" +] diff --git a/src/abi/liquidityPools/Currency.abi.json b/src/abi/liquidityPools/Currency.abi.json new file mode 100644 index 0000000..dcf6ed0 --- /dev/null +++ b/src/abi/liquidityPools/Currency.abi.json @@ -0,0 +1 @@ +["function approve(address, uint) external returns (bool)", "function transfer(address, uint) external returns (bool)"] diff --git a/src/abi/liquidityPools/Gateway.abi.json b/src/abi/liquidityPools/Gateway.abi.json new file mode 100644 index 0000000..4dac24f --- /dev/null +++ b/src/abi/liquidityPools/Gateway.abi.json @@ -0,0 +1 @@ +["function investmentManager() view returns (address)"] diff --git a/src/abi/liquidityPools/InvestmentManager.abi.json b/src/abi/liquidityPools/InvestmentManager.abi.json new file mode 100644 index 0000000..06adf46 --- /dev/null +++ b/src/abi/liquidityPools/InvestmentManager.abi.json @@ -0,0 +1 @@ +["function poolManager() view returns (address)"] diff --git a/src/abi/liquidityPools/LiquidityPool.abi.json b/src/abi/liquidityPools/LiquidityPool.abi.json new file mode 100644 index 0000000..ab4b837 --- /dev/null +++ b/src/abi/liquidityPools/LiquidityPool.abi.json @@ -0,0 +1,15 @@ +[ + "event DepositRequest(address indexed, address indexed, uint256 indexed, address, uint256)", + "event RedeemRequest(address indexed, address indexed, uint256 indexed, address, uint256)", + "event CancelDepositRequest(address indexed)", + "event CancelRedeemRequest(address indexed)", + "function mint(uint256, address) public returns (uint256)", + "function withdraw(uint256, address, address) public returns (uint256)", + "function requestDeposit(uint256, address, address) public", + "function requestRedeem(uint256, address, address) public", + "function requestDepositWithPermit(uint256, address, bytes, uint256, uint8, bytes32, bytes32) public", + "function cancelDepositRequest(uint256, address) public", + "function cancelRedeemRequest(uint256, address) public", + "function claimCancelDepositRequest(uint256, address, address) public", + "function claimCancelRedeemRequest(uint256, address, address) public" +] diff --git a/src/abi/liquidityPools/Permit.abi.json b/src/abi/liquidityPools/Permit.abi.json new file mode 100644 index 0000000..f78c411 --- /dev/null +++ b/src/abi/liquidityPools/Permit.abi.json @@ -0,0 +1 @@ +["function PERMIT_TYPEHASH() view returns (bytes32)"] diff --git a/src/abi/liquidityPools/PoolManager.abi.json b/src/abi/liquidityPools/PoolManager.abi.json new file mode 100644 index 0000000..1d14a41 --- /dev/null +++ b/src/abi/liquidityPools/PoolManager.abi.json @@ -0,0 +1,5 @@ +[ + "function pools(uint64) view returns (uint64, uint256)", + "function deployTranche(uint64, bytes16) public", + "function deployVault(uint64, bytes16, address) public" +] diff --git a/src/abi/liquidityPools/Router.abi.json b/src/abi/liquidityPools/Router.abi.json new file mode 100644 index 0000000..e43227e --- /dev/null +++ b/src/abi/liquidityPools/Router.abi.json @@ -0,0 +1 @@ +["function gateway() view returns (address)"] diff --git a/src/abi/liquidityPools/index.ts b/src/abi/liquidityPools/index.ts new file mode 100644 index 0000000..87ed15f --- /dev/null +++ b/src/abi/liquidityPools/index.ts @@ -0,0 +1,20 @@ +import { parseAbi } from 'viem' +import CentrifugeRouter from './CentrifugeRouter.abi.json' +import Currency from './Currency.abi.json' +import Gateway from './Gateway.abi.json' +import InvestmentManager from './InvestmentManager.abi.json' +import LiquidityPool from './LiquidityPool.abi.json' +import Permit from './Permit.abi.json' +import PoolManager from './PoolManager.abi.json' +import Router from './Router.abi.json' + +export const ABI = { + CentrifugeRouter: parseAbi(CentrifugeRouter), + Currency: parseAbi(Currency), + Gateway: parseAbi(Gateway), + InvestmentManager: parseAbi(InvestmentManager), + LiquidityPool: parseAbi(LiquidityPool), + Permit: parseAbi(Permit), + PoolManager: parseAbi(PoolManager), + Router: parseAbi(Router), +} diff --git a/src/config/chains.ts b/src/config/chains.ts new file mode 100644 index 0000000..404880d --- /dev/null +++ b/src/config/chains.ts @@ -0,0 +1,3 @@ +import { arbitrum, base, baseSepolia, celo, mainnet, sepolia } from 'viem/chains' + +export const chains = [mainnet, sepolia, base, baseSepolia, arbitrum, celo] diff --git a/src/config/lp.ts b/src/config/lp.ts new file mode 100644 index 0000000..1b38dd6 --- /dev/null +++ b/src/config/lp.ts @@ -0,0 +1,32 @@ +type LPConfig = { + centrifugeRouter: `0x${string}` + router: `0x${string}` +} +export const lpConfig: Record = { + // Testnet + 11155111: { + centrifugeRouter: '0x723635430aa191ef5f6f856415f41b1a4d81dd7a', + router: '0x130ce3f3c17b4458d6d4dfdf58a86aa2d261662e', + }, + 84532: { + centrifugeRouter: '0x723635430aa191ef5f6f856415f41b1a4d81dd7a', + router: '0xec55db8b44088198a2d72da798535bffb64fba5c', + }, + // Mainnet + 1: { + centrifugeRouter: '0x2F445BA946044C5F508a63eEaF7EAb673c69a1F4', + router: '0x85bafcadea202258e3512ffbc3e2c9ee6ad56365', + }, + 42161: { + centrifugeRouter: '0x2F445BA946044C5F508a63eEaF7EAb673c69a1F4', + router: '0x85bafcadea202258e3512ffbc3e2c9ee6ad56365', + }, + 8453: { + centrifugeRouter: '0xF35501E7fC4a076E744dbAFA883CED74CCF5009d', + router: '0x30e34260b895cae34a1cfb185271628c53311cf3', + }, + 42220: { + centrifugeRouter: '0x5a00C4fF931f37202aD4Be1FDB297E9EDc1CBb33', + router: '0xe4e34083a49df72e634121f32583c9ea59191cca', + }, +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..cdd3b68 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..e658391 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1 @@ +export type HexString = `0x${string}` diff --git a/src/types/query.ts b/src/types/query.ts new file mode 100644 index 0000000..2e16d0b --- /dev/null +++ b/src/types/query.ts @@ -0,0 +1,12 @@ +import type { Observable } from 'rxjs' + +export type CentrifugeQueryOptions = { + cacheTime?: number +} + +export type Query = PromiseLike & Observable +export type QueryFn = ( + keys: (string | number)[] | null, + cb: () => Observable, + options?: CentrifugeQueryOptions +) => Query diff --git a/src/utils/pinToApi.ts b/src/utils/pinToApi.ts new file mode 100644 index 0000000..b3a9a69 --- /dev/null +++ b/src/utils/pinToApi.ts @@ -0,0 +1,9 @@ +export async function pinToApi(path: string, url: string, reqInit?: RequestInit) { + const res = await fetch(`${new URL(url).toString()}/${path}`, reqInit) + if (!res.ok) { + const resText = await res.text() + throw new Error(`Error pinning: ${resText}`) + } + const json = await res.json() + return json +} diff --git a/tsconfig.json b/tsconfig.json index eae52e0..754d172 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "declarationMap": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, - "lib": ["es2023"], // , "dom", "dom.iterable" + "lib": ["es2023", "dom", "dom.iterable"], // "importHelpers": true, // "rootDir": "./src", "outDir": "dist" diff --git a/yarn.lock b/yarn.lock index 61f6b2e..4dc425d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,10 +16,8 @@ __metadata: version: 0.0.0-use.local resolution: "@centrifuge/centrifuge-sdk@workspace:." dependencies: - "@types/isomorphic-fetch": "npm:^0.0.36" eslint: "npm:^9.12.0" globals: "npm:^15.11.0" - isomorphic-fetch: "npm:^3.0.0" npm-run-all: "npm:4.1.5" rxjs: "npm:^7.8.1" typescript: "npm:~5.6.3" @@ -215,13 +213,6 @@ __metadata: languageName: node linkType: hard -"@types/isomorphic-fetch@npm:^0.0.36": - version: 0.0.36 - resolution: "@types/isomorphic-fetch@npm:0.0.36" - checksum: 10c0/7594d9cbd43697214331de4266cd0cc3628a37d3a2fb5faed5010a3d7037f331bfb7ddf3cc9819335e46926cfaa9967aa2db8910255ac7538bed396dec145adc - languageName: node - linkType: hard - "@types/json-schema@npm:^7.0.15": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -1398,16 +1389,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-fetch@npm:^3.0.0": - version: 3.0.0 - resolution: "isomorphic-fetch@npm:3.0.0" - dependencies: - node-fetch: "npm:^2.6.1" - whatwg-fetch: "npm:^3.4.1" - checksum: 10c0/511b1135c6d18125a07de661091f5e7403b7640060355d2d704ce081e019bc1862da849482d079ce5e2559b8976d3de7709566063aec1b908369c0b98a2b075b - languageName: node - linkType: hard - "isows@npm:1.0.6": version: 1.0.6 resolution: "isows@npm:1.0.6" @@ -1566,20 +1547,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.1": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: "npm:^5.0.0" - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 - languageName: node - linkType: hard - "normalize-package-data@npm:^2.3.2": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -2112,13 +2079,6 @@ __metadata: languageName: node linkType: hard -"tr46@npm:~0.0.3": - version: 0.0.3 - resolution: "tr46@npm:0.0.3" - checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 - languageName: node - linkType: hard - "ts-api-utils@npm:^1.3.0": version: 1.3.0 resolution: "ts-api-utils@npm:1.3.0" @@ -2293,30 +2253,6 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^3.0.0": - version: 3.0.1 - resolution: "webidl-conversions@npm:3.0.1" - checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db - languageName: node - linkType: hard - -"whatwg-fetch@npm:^3.4.1": - version: 3.6.20 - resolution: "whatwg-fetch@npm:3.6.20" - checksum: 10c0/fa972dd14091321d38f36a4d062298df58c2248393ef9e8b154493c347c62e2756e25be29c16277396046d6eaa4b11bd174f34e6403fff6aaca9fb30fa1ff46d - languageName: node - linkType: hard - -"whatwg-url@npm:^5.0.0": - version: 5.0.0 - resolution: "whatwg-url@npm:5.0.0" - dependencies: - tr46: "npm:~0.0.3" - webidl-conversions: "npm:^3.0.0" - checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 - languageName: node - linkType: hard - "which-boxed-primitive@npm:^1.0.2": version: 1.0.2 resolution: "which-boxed-primitive@npm:1.0.2" From f901413f89763f9561ffb779b4c9e3f33c796308 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Tue, 15 Oct 2024 13:31:24 +0200 Subject: [PATCH 02/53] abi --- .../liquidityPools/CentrifugeRouter.abi.json | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/abi/liquidityPools/CentrifugeRouter.abi.json b/src/abi/liquidityPools/CentrifugeRouter.abi.json index e721d35..54c7389 100644 --- a/src/abi/liquidityPools/CentrifugeRouter.abi.json +++ b/src/abi/liquidityPools/CentrifugeRouter.abi.json @@ -1,5 +1,43 @@ [ - "function estimate(bytes) view returns (uint256)", - "function _transferTrancheTokens(address,uint8,uint64,address,uint128,uint256) external payable returns (bool)", - "function transferTrancheTokens(address,uint8,uint64,bytes32,uint128,uint256) public payable returns (bool)" + "constructor(address escrow_, address gateway_, address poolManager_)", + "event Deny(address indexed user)", + "event ExecuteLockedDepositRequest(address indexed vault, address indexed controller, address sender)", + "event LockDepositRequest(address indexed vault, address indexed controller, address indexed owner, address sender, uint256 amount)", + "event Rely(address indexed user)", + "event UnlockDepositRequest(address indexed vault, address indexed controller, address indexed receiver)", + "function INITIATOR_SLOT() view returns (bytes32)", + "function cancelDepositRequest(address vault, uint256 topUpAmount) payable", + "function cancelRedeemRequest(address vault, uint256 topUpAmount) payable", + "function claimCancelDepositRequest(address vault, address receiver, address controller) payable", + "function claimCancelRedeemRequest(address vault, address receiver, address controller) payable", + "function claimDeposit(address vault, address receiver, address controller) payable", + "function claimRedeem(address vault, address receiver, address controller) payable", + "function deny(address user)", + "function disable(address vault)", + "function enable(address vault)", + "function enableLockDepositRequest(address vault, uint256 amount) payable", + "function escrow() view returns (address)", + "function estimate(bytes payload) view returns (uint256 amount)", + "function executeLockedDepositRequest(address vault, address controller, uint256 topUpAmount) payable", + "function gateway() view returns (address)", + "function getVault(uint64 poolId, bytes16 trancheId, address asset) view returns (address)", + "function hasPermissions(address vault, address controller) view returns (bool)", + "function isEnabled(address vault, address controller) view returns (bool)", + "function lockDepositRequest(address vault, uint256 amount, address controller, address owner) payable", + "function lockedRequests(address controller, address vault) view returns (uint256 amount)", + "function multicall(bytes[] data) payable", + "function permit(address asset, address spender, uint256 assets, uint256 deadline, uint8 v, bytes32 r, bytes32 s) payable", + "function poolManager() view returns (address)", + "function recoverTokens(address token, address to, uint256 amount)", + "function rely(address user)", + "function requestDeposit(address vault, uint256 amount, address controller, address owner, uint256 topUpAmount) payable", + "function requestRedeem(address vault, uint256 amount, address controller, address owner, uint256 topUpAmount) payable", + "function transferAssets(address asset, address recipient, uint128 amount, uint256 topUpAmount) payable", + "function transferAssets(address transferProxy, address asset, uint256 topUpAmount) payable", + "function transferAssets(address asset, bytes32 recipient, uint128 amount, uint256 topUpAmount) payable", + "function transferTrancheTokens(address vault, uint8 domain, uint64 chainId, bytes32 recipient, uint128 amount, uint256 topUpAmount) payable", + "function unlockDepositRequest(address vault, address receiver) payable", + "function unwrap(address wrapper, uint256 amount, address receiver) payable", + "function wards(address) view returns (uint256)", + "function wrap(address wrapper, uint256 amount, address receiver, address owner) payable" ] From 8c8a0d22ff0ade36936c01ffd203f7830f2398fc Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Thu, 17 Oct 2024 16:49:25 +0200 Subject: [PATCH 03/53] events and subquery --- src/Account.ts | 44 ++++ src/Centrifuge.ts | 213 +++++++++++++----- src/Pool.ts | 52 ++++- src/PoolDomain.ts | 50 ++-- .../CentrifugeRouter.abi.json | 0 src/abi/Currency.abi.json | 7 + src/abi/{liquidityPools => }/Gateway.abi.json | 0 .../InvestmentManager.abi.json | 0 .../LiquidityPool.abi.json | 0 src/abi/{liquidityPools => }/Permit.abi.json | 0 src/abi/PoolManager.abi.json | 55 +++++ src/abi/{liquidityPools => }/Router.abi.json | 0 src/abi/{liquidityPools => }/index.ts | 0 src/abi/liquidityPools/Currency.abi.json | 1 - src/abi/liquidityPools/PoolManager.abi.json | 5 - src/types/query.ts | 10 +- src/utils/rx.ts | 38 ++++ 17 files changed, 385 insertions(+), 90 deletions(-) create mode 100644 src/Account.ts rename src/abi/{liquidityPools => }/CentrifugeRouter.abi.json (100%) create mode 100644 src/abi/Currency.abi.json rename src/abi/{liquidityPools => }/Gateway.abi.json (100%) rename src/abi/{liquidityPools => }/InvestmentManager.abi.json (100%) rename src/abi/{liquidityPools => }/LiquidityPool.abi.json (100%) rename src/abi/{liquidityPools => }/Permit.abi.json (100%) create mode 100644 src/abi/PoolManager.abi.json rename src/abi/{liquidityPools => }/Router.abi.json (100%) rename src/abi/{liquidityPools => }/index.ts (100%) delete mode 100644 src/abi/liquidityPools/Currency.abi.json delete mode 100644 src/abi/liquidityPools/PoolManager.abi.json create mode 100644 src/utils/rx.ts diff --git a/src/Account.ts b/src/Account.ts new file mode 100644 index 0000000..c5e2f72 --- /dev/null +++ b/src/Account.ts @@ -0,0 +1,44 @@ +import { defer } from 'rxjs' +import { ABI } from './abi/index.js' +import type { Centrifuge } from './Centrifuge.js' +import { Entity } from './Entity.js' +import type { HexString } from './types/index.js' +import { repeatOnEvents } from './utils/rx.js' + +const tUSD = '0x8503b4452Bf6238cC76CdbEE223b46d7196b1c93' + +export class Account extends Entity { + constructor(_root: Centrifuge, public accountId: HexString, public chainId: number) { + super(_root, ['account', accountId, chainId]) + } + + balances() { + return this._query(['balances'], () => { + return defer(async () => { + const client = this._root.getClient(this.chainId)! + const balance = (await client.readContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'balanceOf', + args: [this.accountId], + })) as bigint + return balance + }).pipe( + repeatOnEvents( + this._root, + { + address: tUSD, + abi: ABI.Currency, + eventName: 'Transfer', + filter: (events) => { + return events.some((event) => { + return event.args.from === this.accountId || event.args.to === this.accountId + }) + }, + }, + this.chainId + ) + ) + }) + } +} diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index bf1001b..99f8fc4 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -1,59 +1,77 @@ -import type { MonoTypeOperatorFunction, Observable } from 'rxjs' -import { firstValueFrom, map, of, ReplaySubject, share, timer } from 'rxjs' -import { createClient, http, type Client } from 'viem' +import type { Observable } from 'rxjs' +import { + defaultIfEmpty, + defer, + filter, + firstValueFrom, + identity, + isObservable, + map, + mergeMap, + of, + Subject, + tap, + using, +} from 'rxjs' +import { fromFetch } from 'rxjs/fetch' +import { + createPublicClient, + http, + parseEventLogs, + type Abi, + type PublicClient, + type WatchEventOnLogsParameter, +} from 'viem' +import { Account } from './Account.js' import { chains } from './config/chains.js' import { Pool } from './Pool.js' +import type { HexString } from './types/index.js' import type { CentrifugeQueryOptions, Query } from './types/query.js' -import { pinToApi } from './utils/pinToApi.js' +import { shareReplayWithDelayedReset } from './utils/rx.js' export type Config = { environment: 'mainnet' | 'demo' | 'dev' subqueryUrl: string - ipfsHost: string - pinFile: (b64URI: string) => Promise<{ - uri: string - }> - pinJson: (json: string) => Promise<{ - uri: string - }> +} +type DerivedConfig = Config & { + defaultChain: number } export type UserProvidedConfig = Partial const envConfig = { mainnet: { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', - pinningApiUrl: 'https://europe-central2-centrifuge-production-x.cloudfunctions.net/pinning-api-production', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8ed99a9a115349bbbc01dcf3a24edc96', + defaultChain: 1, }, demo: { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', - pinningApiUrl: 'https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-demo', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', + defaultChain: 11155111, }, dev: { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', - pinningApiUrl: 'https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-demo', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', + defaultChain: 11155111, }, } const defaultConfig = { environment: 'mainnet', - ipfsHost: 'https://metadata.centrifuge.io', } satisfies UserProvidedConfig export class Centrifuge { - #config: Config + #config: DerivedConfig get config() { return this.#config } - #clients = new Map() - getClient(chainId: number) { - return this.#clients.get(chainId) + #clients = new Map>() + getClient(chainId?: number) { + return this.#clients.get(chainId ?? this.config.defaultChain) } get chains() { return [...this.#clients.keys()] @@ -64,25 +82,14 @@ export class Centrifuge { this.#config = { ...defaultConfig, subqueryUrl: defaultConfigForEnv.subqueryUrl, - pinFile: (b64URI) => - pinToApi('pinFile', defaultConfigForEnv.pinningApiUrl, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ uri: b64URI }), - }), - pinJson: (json) => - pinToApi('pinJson', defaultConfigForEnv.pinningApiUrl, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ json }), - }), + defaultChain: defaultConfigForEnv.defaultChain, ...config, } Object.freeze(this.#config) chains .filter((chain) => (this.#config.environment === 'mainnet' ? !chain.testnet : chain.testnet)) .forEach((chain) => { - this.#clients.set(chain.id, createClient({ chain, transport: http(), batch: { multicall: true } })) + this.#clients.set(chain.id, createPublicClient({ chain, transport: http(), batch: { multicall: true } })) }) } @@ -90,35 +97,135 @@ export class Centrifuge { return this._query(null, () => of(new Pool(this, id))) } + account(address: HexString, chainId?: number) { + return this._query(null, () => of(new Account(this, address, chainId ?? this.config.defaultChain))) + } + + events(chainId?: number) { + const cid = chainId ?? this.config.defaultChain + return this._query( + ['events', cid], + () => + using( + () => { + const subject = new Subject() + const unwatch = this.getClient(cid)!.watchEvent({ + onLogs: (logs) => subject.next(logs), + }) + return { + unsubscribe: unwatch, + subject, + } + }, + ({ subject }: any) => subject as Subject + ).pipe(tap((logs) => console.log('logs', logs))), + { cache: false } // Only emit new events + ).pipe(filter((logs) => logs.length > 0)) + } + + filteredEvents(address: string | string[], abi: Abi | Abi[], eventName: string | string[], chainId?: number) { + const addresses = (Array.isArray(address) ? address : [address]).map((a) => a.toLowerCase()) + const eventNames = Array.isArray(eventName) ? eventName : [eventName] + return this.events(chainId).pipe( + map((logs) => { + const parsed = parseEventLogs({ + abi: abi.flat(), + eventName: eventNames, + logs, + }) + const filtered = parsed.filter((log) => (addresses.length ? addresses.includes(log.address) : true)) + + return filtered as ((typeof filtered)[0] & { args: any })[] + }), + filter((logs) => logs.length > 0) + ) + } + + _getSubqueryObservable(query: string, variables?: any) { + return fromFetch(this.config.subqueryUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ query, variables }), + selector: async (res) => { + const { data, errors } = await res.json() + if (errors?.length) { + throw errors + } + return data as T + }, + }) + } + + _querySubquery( + keys: (string | number)[] | null, + query: string, + variables: any, + postProcess: (val: Result) => Return + ): Query + _querySubquery( + keys: (string | number)[] | null, + query: string, + variables?: any, + postProcess?: undefined + ): Query + _querySubquery( + keys: (string | number)[] | null, + query: string, + variables?: any, + postProcess?: (val: Result) => Return + ) { + return this._query(keys, () => this._getSubqueryObservable(query, variables).pipe(map(postProcess ?? identity)), { + valueCacheTime: 300, + }) + } + #memoized = new Map() - _memoizeWith(keys: (string | number)[], cb: () => T): T { + #memoizeWith(keys: (string | number)[], callback: () => T): T { const cacheKey = JSON.stringify(keys) - if (this.#memoized.has(cacheKey)) { return this.#memoized.get(cacheKey) } - - const result = cb() + const result = callback() this.#memoized.set(cacheKey, result) - return result } - _query(keys: (string | number)[] | null, cb: () => Observable, options?: CentrifugeQueryOptions): Query { + _query( + keys: (string | number)[] | null, + observableCallback: () => Observable, + options?: CentrifugeQueryOptions + ): Query { function get() { - const $ = cb().pipe( - keys - ? shareReplayWithDelayedReset({ bufferSize: 1, resetDelay: options?.cacheTime ?? 20000 }) - : map((val) => val) + function createShared() { + return observableCallback().pipe( + keys + ? shareReplayWithDelayedReset({ + bufferSize: options?.cache ?? true ? 1 : 0, + resetDelay: (options?.observableCacheTime ?? 60) * 1000, + windowTime: (options?.valueCacheTime ?? Infinity) * 1000, + }) + : identity + ) + } + + // Create shared observable. Recreate it if the previously shared observable has completed + // and no longer has a cached value, which can happen with a finite `valueCacheTime`. + const $query = createShared().pipe( + defaultIfEmpty(defer(createShared)), + mergeMap((d) => (isObservable(d) ? d : of(d))) ) - const obj: Query = Object.assign($, { + + const thenableQuery: Query = Object.assign($query, { then(onfulfilled: (value: T) => any, onrejected: (reason: any) => any) { - return firstValueFrom($).then(onfulfilled, onrejected) + return firstValueFrom($query).then(onfulfilled, onrejected) }, }) - return obj + return thenableQuery } - return keys ? this._memoizeWith(keys, get) : get() + return keys ? this.#memoizeWith(keys, get) : get() } _makeQuery(baseKeys: (string | number)[]) { @@ -126,17 +233,3 @@ export class Centrifuge { this._query(keys ? [...baseKeys, ...keys] : null, cb, options) } } - -export function shareReplayWithDelayedReset(config?: { - bufferSize?: number - windowTime?: number - resetDelay?: number -}): MonoTypeOperatorFunction { - const { bufferSize = Infinity, windowTime = Infinity, resetDelay = 1000 } = config ?? {} - return share({ - connector: () => new ReplaySubject(bufferSize, windowTime), - resetOnError: true, - resetOnComplete: false, - resetOnRefCountZero: isFinite(resetDelay) ? () => timer(resetDelay) : false, - }) -} diff --git a/src/Pool.ts b/src/Pool.ts index 784fa4c..21a60b9 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -1,12 +1,11 @@ import { catchError, combineLatest, map, of, switchMap, timeout } from 'rxjs' import type { Centrifuge } from './Centrifuge.js' +import { Entity } from './Entity.js' import { PoolDomain } from './PoolDomain.js' -import type { QueryFn } from './types/query.js' -export class Pool { - private _query: QueryFn - constructor(private _root: Centrifuge, public id: string) { - this._query = this._root._makeQuery(['pool', this.id]) +export class Pool extends Entity { + constructor(_root: Centrifuge, public id: string) { + super(_root, ['pool', id]) } domains() { @@ -19,6 +18,49 @@ export class Pool { }) } + // return this._query(['tranches'], () => { + // return this._root + // ._getSubqueryObservable<{ pool: { tranches: { nodes: { trancheId: string }[] } } }>( + // `query($poolId: String!) { + // pool(id: $poolId) { + // tranches { + // nodes { + // trancheId + // } + // } + // } + // }`, + // { + // poolId: this.id, + // } + // ) + // .pipe( + // map((data) => { + // return data.pool.tranches.nodes.map((node) => node.trancheId) + // }) + // ) + // }) + tranches() { + return this._root._querySubquery<{ pool: { tranches: { nodes: { trancheId: string }[] } } }>( + ['tranches', this.id], + `query($poolId: String!) { + pool(id: $poolId) { + tranches { + nodes { + trancheId + } + } + } + }`, + { + poolId: this.id, + }, + (data) => { + return data.pool.tranches.nodes.map((node) => node.trancheId) + } + ) + } + activeDomains() { return this._query(null, () => { return this.domains().pipe( diff --git a/src/PoolDomain.ts b/src/PoolDomain.ts index d30e415..5f4b80c 100644 --- a/src/PoolDomain.ts +++ b/src/PoolDomain.ts @@ -1,16 +1,16 @@ -import { defer, switchMap, tap } from 'rxjs' +import { defer, of, switchMap, tap } from 'rxjs' import { getContract } from 'viem' -import { ABI } from './abi/liquidityPools/index.js' +import { ABI } from './abi/index.js' import type { Centrifuge } from './Centrifuge.js' import { lpConfig } from './config/lp.js' +import { Entity } from './Entity.js' import type { Pool } from './Pool.js' import type { HexString } from './types/index.js' -import type { QueryFn } from './types/query.js' +import { repeatOnEvents } from './utils/rx.js' -export class PoolDomain { - private _query: QueryFn - constructor(private _root: Centrifuge, public pool: Pool, public chainId: number) { - this._query = this._root._makeQuery(['pool', this.pool.id, 'domain', this.chainId]) +export class PoolDomain extends Entity { + constructor(centrifuge: Centrifuge, public pool: Pool, public chainId: number) { + super(centrifuge, ['pool', pool.id, 'domain', chainId]) } manager() { @@ -26,7 +26,7 @@ export class PoolDomain { console.log('managerAddress', managerAddress) return managerAddress as HexString }), - { cacheTime: Infinity } + { observableCacheTime: Infinity } ) } @@ -44,24 +44,38 @@ export class PoolDomain { }), tap((poolManager) => console.log('poolManager', poolManager)) ), - { cacheTime: Infinity } + { observableCacheTime: Infinity } ) } isActive() { - return this._query( - ['isActive'], - () => - this.poolManager().pipe( - switchMap((manager) => { - return getContract({ + return this._query(['isActive'], () => + this.poolManager().pipe( + switchMap((manager) => { + return of( + getContract({ address: manager, abi: ABI.PoolManager, client: this._root.getClient(this.chainId)!, }).read.isPoolActive!([this.pool.id]) as Promise - }) - ), - { cacheTime: Infinity } + ).pipe( + repeatOnEvents( + this._root, + { + address: manager, + abi: ABI.PoolManager, + eventName: 'AddPool', + filter: (events) => { + return events.some((event) => { + return event.args.poolId === this.pool.id + }) + }, + }, + this.chainId + ) + ) + }) + ) ) } } diff --git a/src/abi/liquidityPools/CentrifugeRouter.abi.json b/src/abi/CentrifugeRouter.abi.json similarity index 100% rename from src/abi/liquidityPools/CentrifugeRouter.abi.json rename to src/abi/CentrifugeRouter.abi.json diff --git a/src/abi/Currency.abi.json b/src/abi/Currency.abi.json new file mode 100644 index 0000000..81cacd9 --- /dev/null +++ b/src/abi/Currency.abi.json @@ -0,0 +1,7 @@ +[ + "event Approval(address indexed owner, address indexed spender, uint256 value)", + "event Transfer(address indexed from, address indexed to, uint256 value)", + "function approve(address, uint) external returns (bool)", + "function transfer(address, uint) external returns (bool)", + "function balanceOf(address) view returns (uint)" +] diff --git a/src/abi/liquidityPools/Gateway.abi.json b/src/abi/Gateway.abi.json similarity index 100% rename from src/abi/liquidityPools/Gateway.abi.json rename to src/abi/Gateway.abi.json diff --git a/src/abi/liquidityPools/InvestmentManager.abi.json b/src/abi/InvestmentManager.abi.json similarity index 100% rename from src/abi/liquidityPools/InvestmentManager.abi.json rename to src/abi/InvestmentManager.abi.json diff --git a/src/abi/liquidityPools/LiquidityPool.abi.json b/src/abi/LiquidityPool.abi.json similarity index 100% rename from src/abi/liquidityPools/LiquidityPool.abi.json rename to src/abi/LiquidityPool.abi.json diff --git a/src/abi/liquidityPools/Permit.abi.json b/src/abi/Permit.abi.json similarity index 100% rename from src/abi/liquidityPools/Permit.abi.json rename to src/abi/Permit.abi.json diff --git a/src/abi/PoolManager.abi.json b/src/abi/PoolManager.abi.json new file mode 100644 index 0000000..1243bf8 --- /dev/null +++ b/src/abi/PoolManager.abi.json @@ -0,0 +1,55 @@ +[ + "constructor(address escrow_, address vaultFactory_, address trancheFactory_)", + "event AddAsset(uint128 indexed assetId, address indexed asset)", + "event AddPool(uint64 indexed poolId)", + "event AddTranche(uint64 indexed poolId, bytes16 indexed trancheId)", + "event AllowAsset(uint64 indexed poolId, address indexed asset)", + "event Deny(address indexed user)", + "event DeployTranche(uint64 indexed poolId, bytes16 indexed trancheId, address indexed tranche)", + "event DeployVault(uint64 indexed poolId, bytes16 indexed trancheId, address indexed asset, address vault)", + "event DisallowAsset(uint64 indexed poolId, address indexed asset)", + "event File(bytes32 indexed what, address data)", + "event PriceUpdate(uint64 indexed poolId, bytes16 indexed trancheId, address indexed asset, uint256 price, uint64 computedAt)", + "event Rely(address indexed user)", + "event RemoveVault(uint64 indexed poolId, bytes16 indexed trancheId, address indexed asset, address vault)", + "event TransferAssets(address indexed asset, address indexed sender, bytes32 indexed recipient, uint128 amount)", + "event TransferTrancheTokens(uint64 indexed poolId, bytes16 indexed trancheId, address indexed sender, uint8 destinationDomain, uint64 destinationId, bytes32 destinationAddress, uint128 amount)", + "function addAsset(uint128 assetId, address asset)", + "function addPool(uint64 poolId)", + "function addTranche(uint64 poolId, bytes16 trancheId, string name, string symbol, uint8 decimals, address hook)", + "function allowAsset(uint64 poolId, uint128 assetId)", + "function assetToId(address) view returns (uint128 assetId)", + "function canTrancheBeDeployed(uint64 poolId, bytes16 trancheId) view returns (bool)", + "function deny(address user)", + "function deployTranche(uint64 poolId, bytes16 trancheId) returns (address)", + "function deployVault(uint64 poolId, bytes16 trancheId, address asset) returns (address)", + "function disallowAsset(uint64 poolId, uint128 assetId)", + "function escrow() view returns (address)", + "function file(bytes32 what, address data)", + "function gasService() view returns (address)", + "function gateway() view returns (address)", + "function getTranche(uint64 poolId, bytes16 trancheId) view returns (address)", + "function getTranchePrice(uint64 poolId, bytes16 trancheId, address asset) view returns (uint128 price, uint64 computedAt)", + "function getVault(uint64 poolId, bytes16 trancheId, uint128 assetId) view returns (address)", + "function getVault(uint64 poolId, bytes16 trancheId, address asset) view returns (address)", + "function getVaultAsset(address vault) view returns (address, bool)", + "function handle(bytes message)", + "function handleTransfer(uint128 assetId, address recipient, uint128 amount)", + "function handleTransferTrancheTokens(uint64 poolId, bytes16 trancheId, address destinationAddress, uint128 amount)", + "function idToAsset(uint128 assetId) view returns (address)", + "function investmentManager() view returns (address)", + "function isAllowedAsset(uint64 poolId, address asset) view returns (bool)", + "function isPoolActive(uint64 poolId) view returns (bool)", + "function recoverTokens(address token, address to, uint256 amount)", + "function rely(address user)", + "function removeVault(uint64 poolId, bytes16 trancheId, address asset)", + "function trancheFactory() view returns (address)", + "function transferAssets(address asset, bytes32 recipient, uint128 amount)", + "function transferTrancheTokens(uint64 poolId, bytes16 trancheId, uint8 destinationDomain, uint64 destinationId, bytes32 recipient, uint128 amount)", + "function updateRestriction(uint64 poolId, bytes16 trancheId, bytes update)", + "function updateTrancheHook(uint64 poolId, bytes16 trancheId, address hook)", + "function updateTrancheMetadata(uint64 poolId, bytes16 trancheId, string name, string symbol)", + "function updateTranchePrice(uint64 poolId, bytes16 trancheId, uint128 assetId, uint128 price, uint64 computedAt)", + "function vaultFactory() view returns (address)", + "function wards(address) view returns (uint256)" +] diff --git a/src/abi/liquidityPools/Router.abi.json b/src/abi/Router.abi.json similarity index 100% rename from src/abi/liquidityPools/Router.abi.json rename to src/abi/Router.abi.json diff --git a/src/abi/liquidityPools/index.ts b/src/abi/index.ts similarity index 100% rename from src/abi/liquidityPools/index.ts rename to src/abi/index.ts diff --git a/src/abi/liquidityPools/Currency.abi.json b/src/abi/liquidityPools/Currency.abi.json deleted file mode 100644 index dcf6ed0..0000000 --- a/src/abi/liquidityPools/Currency.abi.json +++ /dev/null @@ -1 +0,0 @@ -["function approve(address, uint) external returns (bool)", "function transfer(address, uint) external returns (bool)"] diff --git a/src/abi/liquidityPools/PoolManager.abi.json b/src/abi/liquidityPools/PoolManager.abi.json deleted file mode 100644 index 1d14a41..0000000 --- a/src/abi/liquidityPools/PoolManager.abi.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "function pools(uint64) view returns (uint64, uint256)", - "function deployTranche(uint64, bytes16) public", - "function deployVault(uint64, bytes16, address) public" -] diff --git a/src/types/query.ts b/src/types/query.ts index 2e16d0b..cebcc58 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -1,7 +1,15 @@ import type { Observable } from 'rxjs' export type CentrifugeQueryOptions = { - cacheTime?: number + // How long (in seconds) the observable and it's cached value remains cached after the last subscriber has unsubscribed + // Default `30` + observableCacheTime?: number + // How long (in seconds) the cached value remains cached + // Default `Infinity` + valueCacheTime?: number + // Whether to keep and re-emit the last emitted value for new subscribers + // Default `true` + cache?: boolean } export type Query = PromiseLike & Observable diff --git a/src/utils/rx.ts b/src/utils/rx.ts new file mode 100644 index 0000000..0bfd455 --- /dev/null +++ b/src/utils/rx.ts @@ -0,0 +1,38 @@ +import type { MonoTypeOperatorFunction } from 'rxjs' +import { filter, repeat, ReplaySubject, share, Subject, timer } from 'rxjs' +import type { Abi, Log } from 'viem' +import type { Centrifuge } from '../Centrifuge.js' + +export function shareReplayWithDelayedReset(config?: { + bufferSize?: number + windowTime?: number + resetDelay?: number +}): MonoTypeOperatorFunction { + const { bufferSize = Infinity, windowTime = Infinity, resetDelay = 1000 } = config ?? {} + return share({ + connector: () => (bufferSize === 0 ? new Subject() : new ReplaySubject(bufferSize, windowTime)), + resetOnError: true, + resetOnComplete: false, + resetOnRefCountZero: isFinite(resetDelay) ? () => timer(resetDelay) : false, + }) +} + +export function repeatOnEvents( + centrifuge: Centrifuge, + opts: { + address?: string | string[] + abi: Abi | Abi[] + eventName: string | string[] + filter?: (events: (Log & { args: any })[]) => boolean + }, + chainId?: number +): MonoTypeOperatorFunction { + return repeat({ + delay: () => + centrifuge.filteredEvents(opts.address || [], opts.abi, opts.eventName, chainId).pipe( + filter((events) => { + return opts.filter ? opts.filter(events) : true + }) + ), + }) +} From 157226fadd244288bac4f77460a67f9f0107954b Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Thu, 17 Oct 2024 17:15:52 +0200 Subject: [PATCH 04/53] a file --- src/Entity.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/Entity.ts diff --git a/src/Entity.ts b/src/Entity.ts new file mode 100644 index 0000000..6df2ae8 --- /dev/null +++ b/src/Entity.ts @@ -0,0 +1,9 @@ +import type { Centrifuge } from './Centrifuge.js' +import type { QueryFn } from './types/query.js' + +export class Entity { + protected _query: QueryFn + constructor(protected _root: Centrifuge, queryKeys: (string | number)[]) { + this._query = this._root._makeQuery(queryKeys) + } +} From 2e3a34893a83b232a3ec0d27e53c063f5b47eb25 Mon Sep 17 00:00:00 2001 From: sophian Date: Thu, 17 Oct 2024 18:34:56 -0400 Subject: [PATCH 05/53] Update types for querySubquery --- src/Centrifuge.ts | 22 +++++----------------- src/Pool.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 99f8fc4..8e426a0 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -141,7 +141,7 @@ export class Centrifuge { ) } - _getSubqueryObservable(query: string, variables?: any) { + _getSubqueryObservable(query: string, variables?: Record) { return fromFetch(this.config.subqueryUrl, { method: 'POST', headers: { @@ -159,24 +159,12 @@ export class Centrifuge { }) } - _querySubquery( + _querySubquery( keys: (string | number)[] | null, query: string, - variables: any, - postProcess: (val: Result) => Return - ): Query - _querySubquery( - keys: (string | number)[] | null, - query: string, - variables?: any, - postProcess?: undefined - ): Query - _querySubquery( - keys: (string | number)[] | null, - query: string, - variables?: any, - postProcess?: (val: Result) => Return - ) { + variables?: Record, + postProcess?: (data: Result) => Return + ): typeof postProcess extends undefined ? Query : Query { return this._query(keys, () => this._getSubqueryObservable(query, variables).pipe(map(postProcess ?? identity)), { valueCacheTime: 300, }) diff --git a/src/Pool.ts b/src/Pool.ts index 21a60b9..641dc6c 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -41,17 +41,17 @@ export class Pool extends Entity { // ) // }) tranches() { - return this._root._querySubquery<{ pool: { tranches: { nodes: { trancheId: string }[] } } }>( + return this._root._querySubquery<{ pool: { tranches: { nodes: { trancheId: string }[] } } }, string[]>( ['tranches', this.id], `query($poolId: String!) { - pool(id: $poolId) { - tranches { - nodes { - trancheId + pool(id: $poolId) { + tranches { + nodes { + trancheId + } } } - } - }`, + }`, { poolId: this.id, }, From 1d9e7b40e9903b87dc89a866022b4c668b8ac9a8 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Fri, 18 Oct 2024 12:05:41 +0200 Subject: [PATCH 06/53] subquery --- src/Centrifuge.ts | 57 ++++++++++++++++++++++++++++++++++++++--------- src/Entity.ts | 15 ++++++++++--- src/Pool.ts | 4 ++-- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 8e426a0..a1e9a70 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -1,5 +1,6 @@ import type { Observable } from 'rxjs' import { + concatWith, defaultIfEmpty, defer, filter, @@ -10,16 +11,18 @@ import { mergeMap, of, Subject, - tap, using, } from 'rxjs' import { fromFetch } from 'rxjs/fetch' import { createPublicClient, + createWalletClient, + custom, http, parseEventLogs, type Abi, type PublicClient, + type WalletClient, type WatchEventOnLogsParameter, } from 'viem' import { Account } from './Account.js' @@ -38,6 +41,8 @@ type DerivedConfig = Config & { } export type UserProvidedConfig = Partial +type Provider = { request(...args: any): Promise } + const envConfig = { mainnet: { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', @@ -77,6 +82,18 @@ export class Centrifuge { return [...this.#clients.keys()] } + #signer: WalletClient | null = null + setSigner(provider: Provider | null) { + if (!provider) { + this.#signer = null + return + } + this.#signer = createWalletClient({ transport: custom(provider) }) + } + get signer() { + return this.#signer + } + constructor(config: UserProvidedConfig = {}) { const defaultConfigForEnv = envConfig[config?.environment ?? 'mainnet'] this.#config = { @@ -118,7 +135,7 @@ export class Centrifuge { } }, ({ subject }: any) => subject as Subject - ).pipe(tap((logs) => console.log('logs', logs))), + ), { cache: false } // Only emit new events ).pipe(filter((logs) => logs.length > 0)) } @@ -150,6 +167,7 @@ export class Centrifuge { }, body: JSON.stringify({ query, variables }), selector: async (res) => { + console.log('fetched subquery') const { data, errors } = await res.json() if (errors?.length) { throw errors @@ -159,14 +177,25 @@ export class Centrifuge { }) } + _querySubquery( + keys: (string | number)[] | null, + query: string, + variables?: Record + ): Query + _querySubquery( + keys: (string | number)[] | null, + query: string, + variables: Record, + postProcess: (data: Result) => Return + ): Query _querySubquery( keys: (string | number)[] | null, query: string, variables?: Record, postProcess?: (data: Result) => Return - ): typeof postProcess extends undefined ? Query : Query { + ) { return this._query(keys, () => this._getSubqueryObservable(query, variables).pipe(map(postProcess ?? identity)), { - valueCacheTime: 300, + valueCacheTime: 2, }) } @@ -187,8 +216,10 @@ export class Centrifuge { options?: CentrifugeQueryOptions ): Query { function get() { + const sharedSubject = new Subject>() function createShared() { - return observableCallback().pipe( + console.log('createShared', keys) + const $shared = observableCallback().pipe( keys ? shareReplayWithDelayedReset({ bufferSize: options?.cache ?? true ? 1 : 0, @@ -197,12 +228,16 @@ export class Centrifuge { }) : identity ) + sharedSubject.next($shared) + return $shared } - // Create shared observable. Recreate it if the previously shared observable has completed - // and no longer has a cached value, which can happen with a finite `valueCacheTime`. const $query = createShared().pipe( + // For new subscribers, recreate the shared observable if the previously shared observable has completed + // and no longer has a cached value, which can happen with a finite `valueCacheTime`. defaultIfEmpty(defer(createShared)), + // For existing subscribers, merge any newly created shared observable. + concatWith(sharedSubject), mergeMap((d) => (isObservable(d) ? d : of(d))) ) @@ -210,14 +245,14 @@ export class Centrifuge { then(onfulfilled: (value: T) => any, onrejected: (reason: any) => any) { return firstValueFrom($query).then(onfulfilled, onrejected) }, + toPromise() { + return firstValueFrom($query) + }, }) return thenableQuery } return keys ? this.#memoizeWith(keys, get) : get() } - _makeQuery(baseKeys: (string | number)[]) { - return (keys: (string | number)[] | null, cb: () => Observable, options?: CentrifugeQueryOptions) => - this._query(keys ? [...baseKeys, ...keys] : null, cb, options) - } + _transaction() {} } diff --git a/src/Entity.ts b/src/Entity.ts index 6df2ae8..c0928fc 100644 --- a/src/Entity.ts +++ b/src/Entity.ts @@ -1,9 +1,18 @@ +import type { Observable } from 'rxjs' import type { Centrifuge } from './Centrifuge.js' -import type { QueryFn } from './types/query.js' +import type { CentrifugeQueryOptions } from './types/query.js' export class Entity { - protected _query: QueryFn + #baseKeys: (string | number)[] constructor(protected _root: Centrifuge, queryKeys: (string | number)[]) { - this._query = this._root._makeQuery(queryKeys) + this.#baseKeys = queryKeys + } + + protected _query( + keys: (string | number)[] | null, + observableCallback: () => Observable, + options?: CentrifugeQueryOptions + ) { + return this._root._query(keys ? [...this.#baseKeys, ...keys] : null, observableCallback, options) } } diff --git a/src/Pool.ts b/src/Pool.ts index 641dc6c..59613d1 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -41,7 +41,7 @@ export class Pool extends Entity { // ) // }) tranches() { - return this._root._querySubquery<{ pool: { tranches: { nodes: { trancheId: string }[] } } }, string[]>( + return this._root._querySubquery( ['tranches', this.id], `query($poolId: String!) { pool(id: $poolId) { @@ -55,7 +55,7 @@ export class Pool extends Entity { { poolId: this.id, }, - (data) => { + (data: { pool: { tranches: { nodes: { trancheId: string }[] } } }) => { return data.pool.tranches.nodes.map((node) => node.trancheId) } ) From 5a489c83d37d4626d04447a9e03ba357f4295a2d Mon Sep 17 00:00:00 2001 From: sophian Date: Fri, 18 Oct 2024 14:32:28 -0400 Subject: [PATCH 07/53] Add testing setup with mocha/chai using tenderly virtual textnet rpc --- package.json | 8 +- src/Centrifuge.ts | 7 +- src/abi/index.ts | 16 +- src/tests/Centrifuge.test.ts | 15 + tsconfig.json | 17 +- yarn.lock | 1305 +++++++++++++++++++++++++++++++++- 6 files changed, 1341 insertions(+), 27 deletions(-) create mode 100644 src/tests/Centrifuge.test.ts diff --git a/package.json b/package.json index 6ff85d4..d9a7567 100644 --- a/package.json +++ b/package.json @@ -24,15 +24,21 @@ "scripts": { "dev": "tsc -w --importHelpers", "build": "tsc --importHelpers", - "prepare": "yarn build" + "prepare": "yarn build", + "test": "mocha --loader=ts-node/esm 'src/**/*.test.ts'" }, "dependencies": { "rxjs": "^7.8.1" }, "devDependencies": { + "@types/chai": "^5.0.0", + "@types/mocha": "^10.0.9", + "chai": "^5.1.1", "eslint": "^9.12.0", "globals": "^15.11.0", + "mocha": "^10.7.3", "npm-run-all": "4.1.5", + "ts-node": "^10.9.2", "typescript": "~5.6.3", "typescript-eslint": "^8.8.1", "viem": "^2.21.25" diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index a1e9a70..ad6b270 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -34,6 +34,7 @@ import { shareReplayWithDelayedReset } from './utils/rx.js' export type Config = { environment: 'mainnet' | 'demo' | 'dev' + rpcUrl: string subqueryUrl: string } type DerivedConfig = Config & { @@ -48,18 +49,21 @@ const envConfig = { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8ed99a9a115349bbbc01dcf3a24edc96', + rpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/8ed99a9a115349bbbc01dcf3a24edc96', defaultChain: 1, }, demo: { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', + rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/8cd8e043ee8d4001b97a1c37e08fd9dd', defaultChain: 11155111, }, dev: { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', + rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/8cd8e043ee8d4001b97a1c37e08fd9dd', defaultChain: 11155111, }, } @@ -100,13 +104,14 @@ export class Centrifuge { ...defaultConfig, subqueryUrl: defaultConfigForEnv.subqueryUrl, defaultChain: defaultConfigForEnv.defaultChain, + rpcUrl: defaultConfigForEnv.rpcUrl, ...config, } Object.freeze(this.#config) chains .filter((chain) => (this.#config.environment === 'mainnet' ? !chain.testnet : chain.testnet)) .forEach((chain) => { - this.#clients.set(chain.id, createPublicClient({ chain, transport: http(), batch: { multicall: true } })) + this.#clients.set(chain.id, createPublicClient({ chain, transport: http(this.#config.rpcUrl ?? undefined), batch: { multicall: true } })) }) } diff --git a/src/abi/index.ts b/src/abi/index.ts index 87ed15f..e03133f 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -1,12 +1,12 @@ import { parseAbi } from 'viem' -import CentrifugeRouter from './CentrifugeRouter.abi.json' -import Currency from './Currency.abi.json' -import Gateway from './Gateway.abi.json' -import InvestmentManager from './InvestmentManager.abi.json' -import LiquidityPool from './LiquidityPool.abi.json' -import Permit from './Permit.abi.json' -import PoolManager from './PoolManager.abi.json' -import Router from './Router.abi.json' +import CentrifugeRouter from './CentrifugeRouter.abi.json' assert { type: 'json' } +import Currency from './Currency.abi.json' assert { type: 'json' } +import Gateway from './Gateway.abi.json' assert { type: 'json' } +import InvestmentManager from './InvestmentManager.abi.json' assert { type: 'json' } +import LiquidityPool from './LiquidityPool.abi.json' assert { type: 'json' } +import Permit from './Permit.abi.json' assert { type: 'json' } +import PoolManager from './PoolManager.abi.json' assert { type: 'json' } +import Router from './Router.abi.json' assert { type: 'json' } export const ABI = { CentrifugeRouter: parseAbi(CentrifugeRouter), diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts new file mode 100644 index 0000000..16b2065 --- /dev/null +++ b/src/tests/Centrifuge.test.ts @@ -0,0 +1,15 @@ +import { expect } from 'chai'; +import { Centrifuge } from '../Centrifuge.js'; + +describe('Centrifuge', () => { + it('should be able to fetch account and balances', async () => { + const centrifuge = new Centrifuge({ + environment: 'mainnet', + rpcUrl: 'https://virtual.mainnet.rpc.tenderly.co/43639f5a-b12a-489b-aa15-45aba1d060c4', + }); + const account = await centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f'); + const balances = await account.balances(); + console.log('balances', balances); + expect(balances).to.exist; + }); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 754d172..e79bc27 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,8 @@ "moduleDetection": "force", "module": "NodeNext", "target": "es2023", - // "moduleResolution": "NodeNext", - // "esModuleInterop": true, + "moduleResolution": "NodeNext", + "esModuleInterop": true, "verbatimModuleSyntax": true, "strict": true, "isolatedModules": true, @@ -16,7 +16,11 @@ "declarationMap": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, - "lib": ["es2023", "dom", "dom.iterable"], + "lib": [ + "es2023", + "dom", + "dom.iterable" + ], // "importHelpers": true, // "rootDir": "./src", "outDir": "dist" @@ -28,5 +32,8 @@ // "noUnusedLocals": true, // "noUnusedParameters": true, }, - "include": ["src", "types"] -} + "include": [ + "src", + "types" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4dc425d..7101aa8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,10 +16,15 @@ __metadata: version: 0.0.0-use.local resolution: "@centrifuge/centrifuge-sdk@workspace:." dependencies: + "@types/chai": "npm:^5.0.0" + "@types/mocha": "npm:^10.0.9" + chai: "npm:^5.1.1" eslint: "npm:^9.12.0" globals: "npm:^15.11.0" + mocha: "npm:^10.7.3" npm-run-all: "npm:4.1.5" rxjs: "npm:^7.8.1" + ts-node: "npm:^10.9.2" typescript: "npm:~5.6.3" typescript-eslint: "npm:^8.8.1" viem: "npm:^2.21.25" @@ -28,6 +33,15 @@ __metadata: languageName: unknown linkType: soft +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 10c0/05c5368c13b662ee4c122c7bfbe5dc0b613416672a829f3e78bc49a357a197e0218d6e74e7c66cfcd04e15a179acab080bd3c69658c9fbefd0e1ccd950a07fc6 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -135,6 +149,44 @@ __metadata: languageName: node linkType: hard +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.0.3": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2" + checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: 10c0/fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b + languageName: node + linkType: hard + "@noble/curves@npm:1.6.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:~1.6.0": version: 1.6.0 resolution: "@noble/curves@npm:1.6.0" @@ -178,6 +230,35 @@ __metadata: languageName: node linkType: hard +"@npmcli/agent@npm:^2.0.0": + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + "@scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": version: 1.1.9 resolution: "@scure/base@npm:1.1.9" @@ -206,6 +287,41 @@ __metadata: languageName: node linkType: hard +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node10@npm:1.0.11" + checksum: 10c0/28a0710e5d039e0de484bdf85fee883bfd3f6a8980601f4d44066b0a6bcd821d31c4e231d1117731c4e24268bd4cf2a788a6787c12fc7f8d11014c07d582783c + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 10c0/dddca2b553e2bee1308a056705103fc8304e42bb2d2cbd797b84403a223b25c78f2c683ec3e24a095e82cd435387c877239bffcb15a590ba817cd3f6b9a99fd9 + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 10c0/67c1316d065fdaa32525bc9449ff82c197c4c19092b9663b23213c8cbbf8d88b6ed6a17898e0cbc2711950fbfaf40388938c1c748a2ee89f7234fc9e7fe2bf44 + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 10c0/05f8f2734e266fb1839eb1d57290df1664fe2aa3b0fdd685a9035806daa635f7519bf6d5d9b33f6e69dd545b8c46bd6e2b5c79acb2b1f146e885f7f11a42a5bb + languageName: node + linkType: hard + +"@types/chai@npm:^5.0.0": + version: 5.0.0 + resolution: "@types/chai@npm:5.0.0" + checksum: 10c0/fcce55f2bbb8485fc860a1dcbac17c1a685b598cfc91a55d37b65b1642b921cf736caa8cce9dcc530830d900f78ab95cf43db4e118db34a5176f252cacd9e1e8 + languageName: node + linkType: hard + "@types/estree@npm:^1.0.6": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" @@ -220,6 +336,13 @@ __metadata: languageName: node linkType: hard +"@types/mocha@npm:^10.0.9": + version: 10.0.9 + resolution: "@types/mocha@npm:10.0.9" + checksum: 10c0/76dd782ac7e971ea159d4a7fd40c929afa051e040be3f41187ff03a2d7b3279e19828ddaa498ba1757b3e6b91316263bb7640db0e906938275b97a06e087b989 + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:8.8.1": version: 8.8.1 resolution: "@typescript-eslint/eslint-plugin@npm:8.8.1" @@ -336,6 +459,13 @@ __metadata: languageName: node linkType: hard +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: 10c0/f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + languageName: node + linkType: hard + "abitype@npm:1.0.6": version: 1.0.6 resolution: "abitype@npm:1.0.6" @@ -360,6 +490,24 @@ __metadata: languageName: node linkType: hard +"acorn-walk@npm:^8.1.1": + version: 8.3.4 + resolution: "acorn-walk@npm:8.3.4" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10c0/76537ac5fb2c37a64560feaf3342023dadc086c46da57da363e64c6148dc21b57d49ace26f949e225063acb6fb441eabffd89f7a3066de5ad37ab3e328927c62 + languageName: node + linkType: hard + +"acorn@npm:^8.11.0, acorn@npm:^8.4.1": + version: 8.13.0 + resolution: "acorn@npm:8.13.0" + bin: + acorn: bin/acorn + checksum: 10c0/f35dd53d68177c90699f4c37d0bb205b8abe036d955d0eb011ddb7f14a81e6fd0f18893731c457c1b5bd96754683f4c3d80d9a5585ddecaa53cdf84e0b3d68f7 + languageName: node + linkType: hard + "acorn@npm:^8.12.0": version: 8.12.1 resolution: "acorn@npm:8.12.1" @@ -369,6 +517,25 @@ __metadata: languageName: node linkType: hard +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" + dependencies: + debug: "npm:^4.3.4" + checksum: 10c0/e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + languageName: node + linkType: hard + "ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -381,6 +548,27 @@ __metadata: languageName: node linkType: hard +"ansi-colors@npm:^4.1.3": + version: 4.1.3 + resolution: "ansi-colors@npm:4.1.3" + checksum: 10c0/ec87a2f59902f74e61eada7f6e6fe20094a628dab765cfdbd03c3477599368768cffccdb5d3bb19a1b6c99126783a143b1fee31aab729b31ffe5836c7e5e28b9 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.1.0 + resolution: "ansi-regex@npm:6.1.0" + checksum: 10c0/a91daeddd54746338478eef88af3439a7edf30f8e23196e2d6ed182da9add559c601266dbef01c2efa46a958ad6f1f8b176799657616c702b5b02e799e7fd8dc + languageName: node + linkType: hard + "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -390,7 +578,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^4.1.0": +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" dependencies: @@ -399,6 +587,30 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 10c0/070ff801a9d236a6caa647507bdcc7034530604844d64408149a26b9e87c2f97650055c0f049abd1efc024b334635c01f29e0b632b371ac3f26130f4cf65997a + languageName: node + linkType: hard + "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -432,6 +644,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + "available-typed-arrays@npm:^1.0.7": version: 1.0.7 resolution: "available-typed-arrays@npm:1.0.7" @@ -448,6 +667,13 @@ __metadata: languageName: node linkType: hard +"binary-extensions@npm:^2.0.0": + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -467,7 +693,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.3": +"braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -476,6 +702,33 @@ __metadata: languageName: node linkType: hard +"browser-stdout@npm:^1.3.1": + version: 1.3.1 + resolution: "browser-stdout@npm:1.3.1" + checksum: 10c0/c40e482fd82be872b6ea7b9f7591beafbf6f5ba522fe3dade98ba1573a1c29a11101564993e4eb44e5488be8f44510af072df9a9637c739217eb155ceb639205 + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.4 + resolution: "cacache@npm:18.0.4" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: 10c0/6c055bafed9de4f3dcc64ac3dc7dd24e863210902b7c470eb9ce55a806309b3efff78033e3d8b4f7dcc5d467f2db43c6a2857aaaf26f0094b8a351d44c42179f + languageName: node + linkType: hard + "call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -496,6 +749,26 @@ __metadata: languageName: node linkType: hard +"camelcase@npm:^6.0.0": + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 10c0/0d701658219bd3116d12da3eab31acddb3f9440790c0792e0d398f0a520a6a4058018e546862b6fba89d7ae990efaeb97da71e1913e9ebf5a8b5621a3d55c710 + languageName: node + linkType: hard + +"chai@npm:^5.1.1": + version: 5.1.1 + resolution: "chai@npm:5.1.1" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/e7f00e5881e3d5224f08fe63966ed6566bd9fdde175863c7c16dd5240416de9b34c4a0dd925f4fd64ad56256ca6507d32cf6131c49e1db65c62578eb31d4566c + languageName: node + linkType: hard + "chalk@npm:^2.4.1": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -507,7 +780,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0": +"chalk@npm:^4.0.0, chalk@npm:^4.1.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -517,6 +790,57 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: 10c0/979f13eccab306cf1785fa10941a590b4e7ea9916ea2a4f8c87f0316fc3eab07eabefb6e587424ef0f88cbcd3805791f172ea739863ca3d7ce2afc54641c7f0e + languageName: node + linkType: hard + +"chokidar@npm:^3.5.3": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + languageName: node + linkType: hard + +"cliui@npm:^7.0.2": + version: 7.0.4 + resolution: "cliui@npm:7.0.4" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^7.0.0" + checksum: 10c0/6035f5daf7383470cef82b3d3db00bec70afb3423538c50394386ffbbab135e26c3689c41791f911fa71b62d13d3863c712fdd70f0fbdffd938a1e6fd09aac00 + languageName: node + linkType: hard + "color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -556,6 +880,13 @@ __metadata: languageName: node linkType: hard +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: 10c0/157cbc59b2430ae9a90034a5f3a1b398b6738bf510f713edc4d4e45e169bc514d3d99dd34d8d01ca7ae7830b5b8b537e46ae8f3c8f932371b0875c0151d7ec91 + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.5": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -569,7 +900,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.2": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -613,7 +944,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5": version: 4.3.7 resolution: "debug@npm:4.3.7" dependencies: @@ -625,6 +956,20 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^4.0.0": + version: 4.0.0 + resolution: "decamelize@npm:4.0.0" + checksum: 10c0/e06da03fc05333e8cd2778c1487da67ffbea5b84e03ca80449519b8fa61f888714bbc6f459ea963d5641b4aa98832130eb5cd193d90ae9f0a27eee14be8e278d + languageName: node + linkType: hard + +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -654,6 +999,64 @@ __metadata: languageName: node linkType: hard +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: 10c0/81b91f9d39c4eaca068eb0c1eb0e4afbdc5bb2941d197f513dd596b820b956fef43485876226d65d497bebc15666aa2aa82c679e84f65d5f2bfbf14ee46e32c1 + languageName: node + linkType: hard + +"diff@npm:^5.2.0": + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 10c0/aed0941f206fe261ecb258dc8d0ceea8abbde3ace5827518ff8d302f0fc9cc81ce116c4d8f379151171336caf0516b79e01abdc1ed1201b6440d895a66689eb4 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + "error-ex@npm:^1.3.1": version: 1.3.2 resolution: "error-ex@npm:1.3.2" @@ -764,6 +1167,13 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.1.1": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65 + languageName: node + linkType: hard + "escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -895,6 +1305,13 @@ __metadata: languageName: node linkType: hard +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 10c0/160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -976,6 +1393,15 @@ __metadata: languageName: node linkType: hard +"flat@npm:^5.0.2": + version: 5.0.2 + resolution: "flat@npm:5.0.2" + bin: + flat: cli.js + checksum: 10c0/f178b13482f0cd80c7fede05f4d10585b1f2fdebf26e12edc138e32d3150c6ea6482b7f12813a1091143bad52bb6d3596bca51a162257a21163c0ff438baa5fe + languageName: node + linkType: hard + "flatted@npm:^3.2.9": version: 3.3.1 resolution: "flatted@npm:3.3.1" @@ -992,6 +1418,60 @@ __metadata: languageName: node linkType: hard +"foreground-child@npm:^3.1.0": + version: 3.3.0 + resolution: "foreground-child@npm:3.3.0" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 10c0/028f1d41000553fcfa6c4bb5c372963bf3d9bf0b1f25a87d1a6253014343fb69dfb1b42d9625d7cf44c8ba429940f3d0ff718b62105d4d4a4f6ef8ca0a53faa2 + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "function-bind@npm:^1.1.2": version: 1.1.2 resolution: "function-bind@npm:1.1.2" @@ -1018,6 +1498,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + "get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4": version: 1.2.4 resolution: "get-intrinsic@npm:1.2.4" @@ -1042,7 +1529,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2": +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -1060,6 +1547,35 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.4.5 + resolution: "glob@npm:10.4.5" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e + languageName: node + linkType: hard + +"glob@npm:^8.1.0": + version: 8.1.0 + resolution: "glob@npm:8.1.0" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^5.0.1" + once: "npm:^1.3.0" + checksum: 10c0/cb0b5cab17a59c57299376abe5646c7070f8acb89df5595b492dba3bfb43d301a46c01e5695f01154e6553168207cb60d4eaf07d3be4bc3eb9b0457c5c561d0f + languageName: node + linkType: hard + "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -1093,7 +1609,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -1169,6 +1685,15 @@ __metadata: languageName: node linkType: hard +"he@npm:^1.2.0": + version: 1.2.0 + resolution: "he@npm:1.2.0" + bin: + he: bin/he + checksum: 10c0/a27d478befe3c8192f006cdd0639a66798979dfa6e2125c6ac582a19a5ebfec62ad83e8382e6036170d873f46e4536a7e795bf8b95bf7c247f4cc0825ccc8c17 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -1176,6 +1701,42 @@ __metadata: languageName: node linkType: hard +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 10c0/2490e3acec397abeb88807db52cac59102d5ed758feee6df6112ab3ccd8325e8a1ce8bce6f4b66e5470eca102d31e425ace904242e4fa28dbe0c59c4bafa7b2c + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + "ignore@npm:^5.2.0, ignore@npm:^5.3.1": version: 5.3.2 resolution: "ignore@npm:5.3.2" @@ -1200,8 +1761,32 @@ __metadata: languageName: node linkType: hard -"internal-slot@npm:^1.0.7": - version: 1.0.7 +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: "npm:^1.3.0" + wrappy: "npm:1" + checksum: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 + languageName: node + linkType: hard + +"inherits@npm:2": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + languageName: node + linkType: hard + +"internal-slot@npm:^1.0.7": + version: 1.0.7 resolution: "internal-slot@npm:1.0.7" dependencies: es-errors: "npm:^1.3.0" @@ -1211,6 +1796,16 @@ __metadata: languageName: node linkType: hard +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc + languageName: node + linkType: hard + "is-array-buffer@npm:^3.0.4": version: 3.0.4 resolution: "is-array-buffer@npm:3.0.4" @@ -1237,6 +1832,15 @@ __metadata: languageName: node linkType: hard +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + "is-boolean-object@npm:^1.1.0": version: 1.1.2 resolution: "is-boolean-object@npm:1.1.2" @@ -1288,7 +1892,14 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -1297,6 +1908,13 @@ __metadata: languageName: node linkType: hard +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + languageName: node + linkType: hard + "is-negative-zero@npm:^2.0.3": version: 2.0.3 resolution: "is-negative-zero@npm:2.0.3" @@ -1320,6 +1938,13 @@ __metadata: languageName: node linkType: hard +"is-plain-obj@npm:^2.1.0": + version: 2.1.0 + resolution: "is-plain-obj@npm:2.1.0" + checksum: 10c0/e5c9814cdaa627a9ad0a0964ded0e0491bfd9ace405c49a5d63c88b30a162f1512c069d5b80997893c4d0181eadc3fed02b4ab4b81059aba5620bfcdfdeb9c53 + languageName: node + linkType: hard + "is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -1366,6 +1991,13 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^0.1.0": + version: 0.1.0 + resolution: "is-unicode-supported@npm:0.1.0" + checksum: 10c0/00cbe3455c3756be68d2542c416cab888aebd5012781d6819749fefb15162ff23e38501fe681b3d751c73e8ff561ac09a5293eba6f58fdf0178462ce6dcb3453 + languageName: node + linkType: hard + "is-weakref@npm:^1.0.2": version: 1.0.2 resolution: "is-weakref@npm:1.0.2" @@ -1389,6 +2021,13 @@ __metadata: languageName: node linkType: hard +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + "isows@npm:1.0.6": version: 1.0.6 resolution: "isows@npm:1.0.6" @@ -1398,6 +2037,19 @@ __metadata: languageName: node linkType: hard +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9 + languageName: node + linkType: hard + "js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" @@ -1409,6 +2061,13 @@ __metadata: languageName: node linkType: hard +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -1484,6 +2143,57 @@ __metadata: languageName: node linkType: hard +"log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" + dependencies: + chalk: "npm:^4.1.0" + is-unicode-supported: "npm:^0.1.0" + checksum: 10c0/67f445a9ffa76db1989d0fa98586e5bc2fd5247260dafb8ad93d9f0ccd5896d53fb830b0e54dade5ad838b9de2006c826831a3c528913093af20dff8bd24aca6 + languageName: node + linkType: hard + +"loupe@npm:^3.1.0": + version: 3.1.2 + resolution: "loupe@npm:3.1.2" + checksum: 10c0/b13c02e3ddd6a9d5f8bf84133b3242de556512d824dddeea71cce2dbd6579c8f4d672381c4e742d45cf4423d0701765b4a6e5fbc24701def16bc2b40f8daa96a + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.4.3 + resolution: "lru-cache@npm:10.4.3" + checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb + languageName: node + linkType: hard + +"make-error@npm:^1.1.1": + version: 1.3.6 + resolution: "make-error@npm:1.3.6" + checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" + dependencies: + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" + http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" + promise-retry: "npm:^2.0.1" + ssri: "npm:^10.0.0" + checksum: 10c0/df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e + languageName: node + linkType: hard + "memorystream@npm:^0.3.1": version: 0.3.1 resolution: "memorystream@npm:0.3.1" @@ -1517,6 +2227,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.6": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/3defdfd230914f22a8da203747c42ee3c405c39d4d37ffda284dac5e45b7e1f6c49aa8be606509002898e73091ff2a3bbfc59c2c6c71d4660609f63aa92f98e3 + languageName: node + linkType: hard + "minimatch@npm:^9.0.4": version: 9.0.5 resolution: "minimatch@npm:9.0.5" @@ -1526,6 +2245,130 @@ __metadata: languageName: node linkType: hard +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 10c0/9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + languageName: node + linkType: hard + +"mocha@npm:^10.7.3": + version: 10.7.3 + resolution: "mocha@npm:10.7.3" + dependencies: + ansi-colors: "npm:^4.1.3" + browser-stdout: "npm:^1.3.1" + chokidar: "npm:^3.5.3" + debug: "npm:^4.3.5" + diff: "npm:^5.2.0" + escape-string-regexp: "npm:^4.0.0" + find-up: "npm:^5.0.0" + glob: "npm:^8.1.0" + he: "npm:^1.2.0" + js-yaml: "npm:^4.1.0" + log-symbols: "npm:^4.1.0" + minimatch: "npm:^5.1.6" + ms: "npm:^2.1.3" + serialize-javascript: "npm:^6.0.2" + strip-json-comments: "npm:^3.1.1" + supports-color: "npm:^8.1.1" + workerpool: "npm:^6.5.1" + yargs: "npm:^16.2.0" + yargs-parser: "npm:^20.2.9" + yargs-unparser: "npm:^2.0.0" + bin: + _mocha: bin/_mocha + mocha: bin/mocha.js + checksum: 10c0/76a205905ec626262d903954daca31ba8e0dd4347092f627b98b8508dcdb5b30be62ec8f7a405fab3b2e691bdc099721c3291b330c3ee85b8ec40d3d179f8728 + languageName: node + linkType: hard + "ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -1540,6 +2383,13 @@ __metadata: languageName: node linkType: hard +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + languageName: node + linkType: hard + "nice-try@npm:^1.0.4": version: 1.0.5 resolution: "nice-try@npm:1.0.5" @@ -1547,6 +2397,37 @@ __metadata: languageName: node linkType: hard +"node-gyp@npm:latest": + version: 10.2.0 + resolution: "node-gyp@npm:10.2.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^4.1.0" + semver: "npm:^7.3.5" + tar: "npm:^6.2.1" + which: "npm:^4.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/00630d67dbd09a45aee0a5d55c05e3916ca9e6d427ee4f7bc392d2d3dc5fad7449b21fc098dd38260a53d9dcc9c879b36704a1994235d4707e7271af7e9a835b + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.1 + resolution: "nopt@npm:7.2.1" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 + languageName: node + linkType: hard + "normalize-package-data@npm:^2.3.2": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -1559,6 +2440,13 @@ __metadata: languageName: node linkType: hard +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + languageName: node + linkType: hard + "npm-run-all@npm:4.1.5": version: 4.1.5 resolution: "npm-run-all@npm:4.1.5" @@ -1606,6 +2494,15 @@ __metadata: languageName: node linkType: hard +"once@npm:^1.3.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -1638,6 +2535,22 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + languageName: node + linkType: hard + +"package-json-from-dist@npm:^1.0.0": + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 10c0/62ba2785eb655fec084a257af34dbe24292ab74516d6aecef97ef72d4897310bc6898f6c85b5cd22770eaa1ce60d55a0230e150fb6a966e3ecd6c511e23d164b + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -1685,6 +2598,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^1.11.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" + dependencies: + lru-cache: "npm:^10.2.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d + languageName: node + linkType: hard + "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -1694,7 +2617,14 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.3.1": +"pathval@npm:^2.0.0": + version: 2.0.0 + resolution: "pathval@npm:2.0.0" + checksum: 10c0/602e4ee347fba8a599115af2ccd8179836a63c925c23e04bd056d0674a64b39e3a081b643cc7bc0b84390517df2d800a46fcc5598d42c155fe4977095c2f77c5 + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be @@ -1731,6 +2661,23 @@ __metadata: languageName: node linkType: hard +"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10c0/17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + "punycode@npm:^2.1.0": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -1745,6 +2692,15 @@ __metadata: languageName: node linkType: hard +"randombytes@npm:^2.1.0": + version: 2.1.0 + resolution: "randombytes@npm:2.1.0" + dependencies: + safe-buffer: "npm:^5.1.0" + checksum: 10c0/50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3 + languageName: node + linkType: hard + "read-pkg@npm:^3.0.0": version: 3.0.0 resolution: "read-pkg@npm:3.0.0" @@ -1756,6 +2712,15 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + "regexp.prototype.flags@npm:^1.5.2": version: 1.5.3 resolution: "regexp.prototype.flags@npm:1.5.3" @@ -1768,6 +2733,13 @@ __metadata: languageName: node linkType: hard +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -1801,6 +2773,13 @@ __metadata: languageName: node linkType: hard +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + "reusify@npm:^1.0.4": version: 1.0.4 resolution: "reusify@npm:1.0.4" @@ -1838,6 +2817,13 @@ __metadata: languageName: node linkType: hard +"safe-buffer@npm:^5.1.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + languageName: node + linkType: hard + "safe-regex-test@npm:^1.0.3": version: 1.0.3 resolution: "safe-regex-test@npm:1.0.3" @@ -1849,6 +2835,13 @@ __metadata: languageName: node linkType: hard +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + "semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -1858,7 +2851,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.6.0": +"semver@npm:^7.3.5, semver@npm:^7.6.0": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -1867,6 +2860,15 @@ __metadata: languageName: node linkType: hard +"serialize-javascript@npm:^6.0.2": + version: 6.0.2 + resolution: "serialize-javascript@npm:6.0.2" + dependencies: + randombytes: "npm:^2.1.0" + checksum: 10c0/2dd09ef4b65a1289ba24a788b1423a035581bef60817bea1f01eda8e3bda623f86357665fe7ac1b50f6d4f583f97db9615b3f07b2a2e8cbcb75033965f771dd2 + languageName: node + linkType: hard + "set-function-length@npm:^1.2.1": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -1944,6 +2946,41 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.4 + resolution: "socks-proxy-agent@npm:8.0.4" + dependencies: + agent-base: "npm:^7.1.1" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10c0/345593bb21b95b0508e63e703c84da11549f0a2657d6b4e3ee3612c312cb3a907eac10e53b23ede3557c6601d63252103494caa306b66560f43af7b98f53957a + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.3 + resolution: "socks@npm:2.8.3" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 + languageName: node + linkType: hard + "spdx-correct@npm:^3.0.0": version: 3.2.0 resolution: "spdx-correct@npm:3.2.0" @@ -1978,6 +3015,44 @@ __metadata: languageName: node linkType: hard +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.6 + resolution: "ssri@npm:10.0.6" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + "string.prototype.padend@npm:^3.0.0": version: 3.1.6 resolution: "string.prototype.padend@npm:3.1.6" @@ -2024,6 +3099,24 @@ __metadata: languageName: node linkType: hard +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + "strip-bom@npm:^3.0.0": version: 3.0.0 resolution: "strip-bom@npm:3.0.0" @@ -2056,6 +3149,15 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:^8.1.1": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89 + languageName: node + linkType: hard + "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -2063,6 +3165,20 @@ __metadata: languageName: node linkType: hard +"tar@npm:^6.1.11, tar@npm:^6.2.1": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 + languageName: node + linkType: hard + "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -2088,6 +3204,44 @@ __metadata: languageName: node linkType: hard +"ts-node@npm:^10.9.2": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 10c0/5f29938489f96982a25ba650b64218e83a3357d76f7bede80195c65ab44ad279c8357264639b7abdd5d7e75fc269a83daa0e9c62fd8637a3def67254ecc9ddc2 + languageName: node + linkType: hard + "tslib@npm:^2.1.0": version: 2.7.0 resolution: "tslib@npm:2.7.0" @@ -2202,6 +3356,24 @@ __metadata: languageName: node linkType: hard +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 10c0/6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10c0/cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -2211,6 +3383,13 @@ __metadata: languageName: node linkType: hard +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 10c0/bdc36fb8095d3b41df197f5fb6f11e3a26adf4059df3213e3baa93810d8f0cc76f9a74aaefc18b73e91fe7e19154ed6f134eda6fded2e0f1c8d2272ed2d2d391 + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.1": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" @@ -2301,6 +3480,17 @@ __metadata: languageName: node linkType: hard +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 10c0/449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + languageName: node + linkType: hard + "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -2308,6 +3498,42 @@ __metadata: languageName: node linkType: hard +"workerpool@npm:^6.5.1": + version: 6.5.1 + resolution: "workerpool@npm:6.5.1" + checksum: 10c0/58e8e969782292cb3a7bfba823f1179a7615250a0cefb4841d5166234db1880a3d0fe83a31dd8d648329ec92c2d0cd1890ad9ec9e53674bb36ca43e9753cdeac + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 + languageName: node + linkType: hard + "ws@npm:8.18.0": version: 8.18.0 resolution: "ws@npm:8.18.0" @@ -2323,6 +3549,61 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.9": + version: 20.2.9 + resolution: "yargs-parser@npm:20.2.9" + checksum: 10c0/0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72 + languageName: node + linkType: hard + +"yargs-unparser@npm:^2.0.0": + version: 2.0.0 + resolution: "yargs-unparser@npm:2.0.0" + dependencies: + camelcase: "npm:^6.0.0" + decamelize: "npm:^4.0.0" + flat: "npm:^5.0.2" + is-plain-obj: "npm:^2.1.0" + checksum: 10c0/a5a7d6dc157efa95122e16780c019f40ed91d4af6d2bac066db8194ed0ec5c330abb115daa5a79ff07a9b80b8ea80c925baacf354c4c12edd878c0529927ff03 + languageName: node + linkType: hard + +"yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" + dependencies: + cliui: "npm:^7.0.2" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^20.2.2" + checksum: 10c0/b1dbfefa679848442454b60053a6c95d62f2d2e21dd28def92b647587f415969173c6e99a0f3bab4f1b67ee8283bf735ebe3544013f09491186ba9e8a9a2b651 + languageName: node + linkType: hard + +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 10c0/0732468dd7622ed8a274f640f191f3eaf1f39d5349a1b72836df484998d7d9807fbea094e2f5486d6b0cd2414aad5775972df0e68f8604db89a239f0f4bf7443 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" From a3bf57be1a3948cf9902e2f2fa40938a9b1eb379 Mon Sep 17 00:00:00 2001 From: sophian Date: Fri, 18 Oct 2024 18:21:00 -0400 Subject: [PATCH 08/53] Fix accounts testing by switching to demo --- src/tests/Centrifuge.test.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 16b2065..4584967 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -1,15 +1,28 @@ import { expect } from 'chai'; -import { Centrifuge } from '../Centrifuge.js'; +import { Centrifuge } from '../Centrifuge.js'; describe('Centrifuge', () => { - it('should be able to fetch account and balances', async () => { - const centrifuge = new Centrifuge({ - environment: 'mainnet', - rpcUrl: 'https://virtual.mainnet.rpc.tenderly.co/43639f5a-b12a-489b-aa15-45aba1d060c4', + let centrifuge: Centrifuge + + before(async () => { + centrifuge = new Centrifuge({ + environment: 'demo', + rpcUrl: 'https://virtual.sepolia.rpc.tenderly.co/ce7949c5-e956-4913-93bf-83b171163bdb', }); + }); + it("should be connected to mainnet", async () => { + const client = centrifuge.getClient(); + expect(client?.chain.id).to.equal(11155111); + const chains = centrifuge.chains + expect(chains).to.include(11155111); + }); + it('should fetch account and balances', async () => { const account = await centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f'); const balances = await account.balances(); - console.log('balances', balances); expect(balances).to.exist; }); + it('should fetch a pool by id', async () => { + const pool = await centrifuge.pool('4139607887'); + expect(pool).to.exist; + }); }); \ No newline at end of file From a51165d0dce8e834f9b436171ec1fa3c2577713e Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Mon, 21 Oct 2024 13:39:12 +0200 Subject: [PATCH 09/53] transact --- .prettierrc.js | 4 +- eslint.config.js | 1 + package.json | 2 + src/Account.ts | 80 ++++++++++++++++++++++++++++- src/Centrifuge.ts | 106 ++++++++++++++++++++++++++++++--------- src/Entity.ts | 7 ++- src/types/query.ts | 2 +- src/types/transaction.ts | 63 +++++++++++++++++++++++ src/utils/rx.ts | 17 ++++++- src/utils/transaction.ts | 64 +++++++++++++++++++++++ yarn.lock | 27 ++++++++++ 11 files changed, 340 insertions(+), 33 deletions(-) create mode 100644 src/types/transaction.ts create mode 100644 src/utils/transaction.ts diff --git a/.prettierrc.js b/.prettierrc.js index bb30421..41b8f58 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,8 +1,8 @@ -module.exports = { +export default { trailingComma: 'es5', tabWidth: 2, semi: false, singleQuote: true, printWidth: 120, arrowParens: 'always', -}; \ No newline at end of file +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index f80a934..f57f540 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,7 @@ export default [ 'object-shorthand': 'error', 'prefer-template': 'error', '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-this-alias': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { diff --git a/package.json b/package.json index 6ff85d4..dd9aafc 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,14 @@ "prepare": "yarn build" }, "dependencies": { + "eth-permit": "^0.2.3", "rxjs": "^7.8.1" }, "devDependencies": { "eslint": "^9.12.0", "globals": "^15.11.0", "npm-run-all": "4.1.5", + "prettier": "^3.3.3", "typescript": "~5.6.3", "typescript-eslint": "^8.8.1", "viem": "^2.21.25" diff --git a/src/Account.ts b/src/Account.ts index c5e2f72..78d0401 100644 --- a/src/Account.ts +++ b/src/Account.ts @@ -1,14 +1,20 @@ -import { defer } from 'rxjs' +import { defer, switchMap } from 'rxjs' import { ABI } from './abi/index.js' import type { Centrifuge } from './Centrifuge.js' +import { NULL_ADDRESS } from './constants.js' import { Entity } from './Entity.js' import type { HexString } from './types/index.js' import { repeatOnEvents } from './utils/rx.js' +import { doSignMessage, doTransaction, signPermit } from './utils/transaction.js' const tUSD = '0x8503b4452Bf6238cC76CdbEE223b46d7196b1c93' export class Account extends Entity { - constructor(_root: Centrifuge, public accountId: HexString, public chainId: number) { + constructor( + _root: Centrifuge, + public accountId: HexString, + public chainId: number + ) { super(_root, ['account', accountId, chainId]) } @@ -41,4 +47,74 @@ export class Account extends Entity { ) }) } + + transfer(to: HexString, amount: bigint) { + return this._transact( + 'Transfer', + async ({ walletClient }) => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }), + this.chainId + ) + } + + transfer2(to: HexString, amount: bigint) { + return this._transact( + 'Transfer', + ({ walletClient }) => + this.balances().pipe( + switchMap((balance) => { + console.log('balance', balance) + return walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + }) + ), + this.chainId + ) + } + + transfer3(to: HexString, amount: bigint) { + return this._transact(async function* ({ walletClient, publicClient, chainId, signingAddress, signer }) { + const permit = yield* doSignMessage('Sign Permit', () => { + return signPermit(walletClient, signer, chainId, signingAddress, tUSD, NULL_ADDRESS, amount) + }) + console.log('permit', permit) + yield* doTransaction('Transfer', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + ) + }, this.chainId) + } + + transfer4(to: HexString, amount: bigint) { + return this._transact( + ({ walletClient, publicClient }) => + this.balances().pipe( + switchMap(async function* (balance) { + console.log('balance', balance) + yield* doTransaction('Transfer', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + ) + }) + ), + this.chainId + ) + } } diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index a1e9a70..83c74eb 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -21,8 +21,8 @@ import { http, parseEventLogs, type Abi, + type Chain, type PublicClient, - type WalletClient, type WatchEventOnLogsParameter, } from 'viem' import { Account } from './Account.js' @@ -30,7 +30,9 @@ import { chains } from './config/chains.js' import { Pool } from './Pool.js' import type { HexString } from './types/index.js' import type { CentrifugeQueryOptions, Query } from './types/query.js' -import { shareReplayWithDelayedReset } from './utils/rx.js' +import type { OperationStatus, Signer, TransactionCallbackParams } from './types/transaction.js' +import { makeThenable, shareReplayWithDelayedReset } from './utils/rx.js' +import { doTransaction } from './utils/transaction.js' export type Config = { environment: 'mainnet' | 'demo' | 'dev' @@ -41,8 +43,6 @@ type DerivedConfig = Config & { } export type UserProvidedConfig = Partial -type Provider = { request(...args: any): Promise } - const envConfig = { mainnet: { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', @@ -74,21 +74,20 @@ export class Centrifuge { return this.#config } - #clients = new Map>() + #clients = new Map>() getClient(chainId?: number) { return this.#clients.get(chainId ?? this.config.defaultChain) } get chains() { return [...this.#clients.keys()] } + getChainConfig(chainId?: number) { + return this.getClient(chainId ?? this.config.defaultChain)!.chain + } - #signer: WalletClient | null = null - setSigner(provider: Provider | null) { - if (!provider) { - this.#signer = null - return - } - this.#signer = createWalletClient({ transport: custom(provider) }) + #signer: Signer | null = null + setSigner(signer: Signer | null) { + this.#signer = signer } get signer() { return this.#signer @@ -106,7 +105,10 @@ export class Centrifuge { chains .filter((chain) => (this.#config.environment === 'mainnet' ? !chain.testnet : chain.testnet)) .forEach((chain) => { - this.#clients.set(chain.id, createPublicClient({ chain, transport: http(), batch: { multicall: true } })) + this.#clients.set( + chain.id, + createPublicClient({ chain, transport: http(), batch: { multicall: true } }) + ) }) } @@ -222,7 +224,7 @@ export class Centrifuge { const $shared = observableCallback().pipe( keys ? shareReplayWithDelayedReset({ - bufferSize: options?.cache ?? true ? 1 : 0, + bufferSize: (options?.cache ?? true) ? 1 : 0, resetDelay: (options?.observableCacheTime ?? 60) * 1000, windowTime: (options?.valueCacheTime ?? Infinity) * 1000, }) @@ -240,19 +242,73 @@ export class Centrifuge { concatWith(sharedSubject), mergeMap((d) => (isObservable(d) ? d : of(d))) ) - - const thenableQuery: Query = Object.assign($query, { - then(onfulfilled: (value: T) => any, onrejected: (reason: any) => any) { - return firstValueFrom($query).then(onfulfilled, onrejected) - }, - toPromise() { - return firstValueFrom($query) - }, - }) - return thenableQuery + return makeThenable($query) } return keys ? this.#memoizeWith(keys, get) : get() } - _transaction() {} + _transact( + title: string, + transactionCallback: (params: TransactionCallbackParams) => Promise | Observable, + chainId?: number + ): Query + _transact( + transactionCallback: ( + params: TransactionCallbackParams + ) => AsyncGenerator | Observable, + chainId?: number + ): Query + _transact(...args: any[]) { + const isSimple = typeof args[0] === 'string' + const title = isSimple ? args[0] : undefined + const callback = (isSimple ? args[1] : args[0]) as ( + params: TransactionCallbackParams + ) => Promise | Observable | AsyncGenerator + const chainId = (isSimple ? args[2] : args[1]) as number + const targetChainId = chainId ?? this.config.defaultChain + + const self = this + async function* transact() { + const { signer } = self + if (!signer) throw new Error('Signer not set') + + const publicClient = self.getClient(targetChainId)! + const bareWalletClient = createWalletClient({ transport: custom(signer) }) + + const [address] = await bareWalletClient.getAddresses() + if (!address) throw new Error('No account selected') + + const chain = self.getChainConfig(targetChainId) + const selectedChain = await bareWalletClient.getChainId() + if (selectedChain !== targetChainId) { + yield { type: 'SwitchingChain', chainId: targetChainId } + await bareWalletClient.switchChain({ id: targetChainId }) + } + + // Recreate the wallet client with the account and chain + const walletClient = createWalletClient({ account: address, chain, transport: custom(signer) }) + + const transaction = callback({ + signingAddress: address, + chain, + chainId: targetChainId, + publicClient, + walletClient, + signer, + }) + if (isSimple) { + yield* doTransaction(title, publicClient, () => + 'then' in transaction ? transaction : firstValueFrom(transaction as Observable) + ) + } else if (Symbol.asyncIterator in transaction) { + yield* transaction + } else if (isObservable(transaction)) { + yield transaction as Observable + } else { + throw new Error('Invalid arguments') + } + } + const $tx = defer(transact).pipe(mergeMap((d) => (isObservable(d) ? d : of(d)))) + return makeThenable($tx, true) + } } diff --git a/src/Entity.ts b/src/Entity.ts index c0928fc..887598c 100644 --- a/src/Entity.ts +++ b/src/Entity.ts @@ -4,8 +4,13 @@ import type { CentrifugeQueryOptions } from './types/query.js' export class Entity { #baseKeys: (string | number)[] - constructor(protected _root: Centrifuge, queryKeys: (string | number)[]) { + _transact: Centrifuge['_transact'] + constructor( + protected _root: Centrifuge, + queryKeys: (string | number)[] + ) { this.#baseKeys = queryKeys + this._transact = this._root._transact.bind(this._root) } protected _query( diff --git a/src/types/query.ts b/src/types/query.ts index cebcc58..6592d4e 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -12,7 +12,7 @@ export type CentrifugeQueryOptions = { cache?: boolean } -export type Query = PromiseLike & Observable +export type Query = PromiseLike & Observable & { toPromise: () => Promise } export type QueryFn = ( keys: (string | number)[] | null, cb: () => Observable, diff --git a/src/types/transaction.ts b/src/types/transaction.ts new file mode 100644 index 0000000..7841ff6 --- /dev/null +++ b/src/types/transaction.ts @@ -0,0 +1,63 @@ +import type { Account, Chain, PublicClient, TransactionReceipt, WalletClient } from 'viem' +import type { HexString } from './index.js' + +export type OperationStatusType = + | 'SwitchingChain' + | 'SigningTransaction' + | 'SigningMessage' + | 'SignedMessage' + | 'TransactionPending' + | 'TransactionConfirmed' + +export type OperationSigningStatus = { + type: 'SigningTransaction' + title: string +} +export type OperationSigningMessageStatus = { + type: 'SigningMessage' + title: string +} +export type OperationSignedMessageStatus = { + type: 'SignedMessage' + title: string + signed: any +} +export type OperationPendingStatus = { + type: 'TransactionPending' + title: string + hash: HexString +} +export type OperationConfirmedStatus = { + type: 'TransactionConfirmed' + title: string + hash: HexString + receipt: TransactionReceipt +} +export type OperationSwitchChainStatus = { + type: 'SwitchingChain' + chainId: number +} + +export type OperationStatus = + | OperationSigningStatus + | OperationSigningMessageStatus + | OperationSignedMessageStatus + | OperationPendingStatus + | OperationConfirmedStatus + | OperationSwitchChainStatus + +export type EIP1193ProviderLike = { + request(...args: any): Promise + // deprecated, but neccessary for eth-permit + send(...args: any): Promise +} +export type Signer = EIP1193ProviderLike + +export type TransactionCallbackParams = { + signingAddress: HexString + chain: Chain + chainId: number + publicClient: PublicClient + walletClient: WalletClient + signer: Signer +} diff --git a/src/utils/rx.ts b/src/utils/rx.ts index 0bfd455..13ac412 100644 --- a/src/utils/rx.ts +++ b/src/utils/rx.ts @@ -1,7 +1,8 @@ -import type { MonoTypeOperatorFunction } from 'rxjs' -import { filter, repeat, ReplaySubject, share, Subject, timer } from 'rxjs' +import type { MonoTypeOperatorFunction, Observable } from 'rxjs' +import { filter, firstValueFrom, lastValueFrom, repeat, ReplaySubject, share, Subject, timer } from 'rxjs' import type { Abi, Log } from 'viem' import type { Centrifuge } from '../Centrifuge.js' +import type { Query } from '../types/query.js' export function shareReplayWithDelayedReset(config?: { bufferSize?: number @@ -36,3 +37,15 @@ export function repeatOnEvents( ), }) } + +export function makeThenable($query: Observable, exhaust = false) { + const thenableQuery: Query = Object.assign($query, { + then(onfulfilled: (value: T) => any, onrejected: (reason: any) => any) { + return (exhaust ? lastValueFrom : firstValueFrom)($query).then(onfulfilled, onrejected) + }, + toPromise() { + return (exhaust ? lastValueFrom : firstValueFrom)($query) + }, + }) + return thenableQuery +} diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts new file mode 100644 index 0000000..b08437b --- /dev/null +++ b/src/utils/transaction.ts @@ -0,0 +1,64 @@ +import { signERC2612Permit } from 'eth-permit' +import type { Account, Chain, PublicClient, WalletClient } from 'viem' +import type { HexString } from '../types/index.js' +import type { OperationStatus, Signer } from '../types/transaction.js' + +export async function* doTransaction( + title: string, + publicClient: PublicClient, + transactionCallback: () => Promise +): AsyncGenerator { + yield { type: 'SigningTransaction', title } + const hash = await transactionCallback() + yield* waitForTransaction(title, publicClient, hash) +} + +export async function* doSignMessage( + title: string, + transactionCallback: () => Promise +): AsyncGenerator { + yield { type: 'SigningMessage', title } + const message = await transactionCallback() + yield { type: 'SignedMessage', title, signed: message } + return message +} + +export async function* waitForTransaction( + title: string, + publicClient: PublicClient, + hash: HexString +): AsyncGenerator { + yield { type: 'TransactionPending', title, hash } + const receipt = await publicClient.waitForTransactionReceipt({ hash }) + yield { type: 'TransactionConfirmed', title, hash, receipt } +} + +export type Permit = { + deadline: number | string + r: string + s: string + v: number +} + +export async function signPermit( + _: WalletClient, + signer: Signer, + chainId: number, + signingAddress: string, + currencyAddress: string, + spender: string, + amount: bigint +) { + let domainOrCurrency: any = currencyAddress + if (currencyAddress.toLowerCase() === '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48') { + // USDC has custom version + domainOrCurrency = { name: 'USD Coin', version: '2', chainId, verifyingContract: currencyAddress } + } else if (chainId === 5 || chainId === 84531 || chainId === 421613 || chainId === 11155111) { + // Assume on testnets the LP currencies are used which have custom domains + domainOrCurrency = { name: 'Centrifuge', version: '1', chainId, verifyingContract: currencyAddress } + } + + const deadline = Math.floor(Date.now() / 1000) + 3600 // 1 hour + const permit = await signERC2612Permit(signer, domainOrCurrency, signingAddress, spender, amount.toString(), deadline) + return permit as Permit +} diff --git a/yarn.lock b/yarn.lock index 4dc425d..c811eb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,8 +17,10 @@ __metadata: resolution: "@centrifuge/centrifuge-sdk@workspace:." dependencies: eslint: "npm:^9.12.0" + eth-permit: "npm:^0.2.3" globals: "npm:^15.11.0" npm-run-all: "npm:4.1.5" + prettier: "npm:^3.3.3" rxjs: "npm:^7.8.1" typescript: "npm:~5.6.3" typescript-eslint: "npm:^8.8.1" @@ -895,6 +897,15 @@ __metadata: languageName: node linkType: hard +"eth-permit@npm:^0.2.3": + version: 0.2.3 + resolution: "eth-permit@npm:0.2.3" + dependencies: + utf8: "npm:^3.0.0" + checksum: 10c0/c43d33e891bebb4ada59ecb6f8cc190d3af25e80fe14edb2c75958a6dc61b2d636d863f54548118f7ef9b1e2a3673c30a34d60fc9570f7d70ee8a1d487054776 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -1731,6 +1742,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^3.3.3": + version: 3.3.3 + resolution: "prettier@npm:3.3.3" + bin: + prettier: bin/prettier.cjs + checksum: 10c0/b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26 + languageName: node + linkType: hard + "punycode@npm:^2.1.0": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -2211,6 +2231,13 @@ __metadata: languageName: node linkType: hard +"utf8@npm:^3.0.0": + version: 3.0.0 + resolution: "utf8@npm:3.0.0" + checksum: 10c0/675d008bab65fc463ce718d5cae8fd4c063540f269e4f25afebce643098439d53e7164bb1f193e0c3852825c7e3e32fbd8641163d19a618dbb53f1f09acb0d5a + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.1": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" From e4d578a79bf806f24f7feedf26cc355c49af0318 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Mon, 21 Oct 2024 14:41:57 +0200 Subject: [PATCH 10/53] readme --- README.md | 57 +++++++++++++++++++++++++++++++++++++++- src/Centrifuge.ts | 32 ++++++++++++++-------- src/types/transaction.ts | 4 +-- src/utils/transaction.ts | 6 ++++- 4 files changed, 84 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6457280..c7863d9 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,10 @@ CentrifugeSDK provides a JavaScript client to interact with the Centrifuge ecosy ## Installation +CentrifugeSDK uses [Viem](https://viem.sh/) under the hood. It's necessary to install it alongside the SDK. + ```bash -npm install --save @centrifuge/centrifuge-sdk +npm install --save @centrifuge/centrifuge-sdk viem ``` ## Init and config @@ -23,3 +25,56 @@ The following config options can be passed on initilization of CentrifugeSDK: #### `TDB` Default value: + +## Queries + +Queries return Promise-like [Observables](https://rxjs.dev/guide/observable). They can be either awaited to get a single value, or subscribed to to get fresh data whenever on-chain data changes. + +```js +try { + const pool = await centrifuge.pools() +} catch (error) { + console.error(error) +} +``` + +```js +const subscription = centrifuge.pools().subscribe( + (pool) => console.log(pool), + (error) => console.error(error) +) +subscription.unsubscribe() +``` + +The returned results are either immutable values, or entities that can be further queried. + +## Transactions + +To perform transactions, you need to set a signer on the `centrifuge` instance. + +```js +centrifuge.setSigner(signer) +``` + +`signer` can be a [EIP1193](https://eips.ethereum.org/EIPS/eip-1193)-compatible provider or a Viem [LocalAccount](https://viem.sh/docs/accounts/local) + +With this you can call transaction methods. Similar to queries they can be awaited to get their final result, or subscribed to get get status updates. + +```js +const pool = centrifuge.pool('1') +try { + const status = await pool.closeEpoch() + console.log(status) +} catch (error) { + console.error(error) +} +``` + +```js +const pool = centrifuge.pool('1') +const subscription = pool.closeEpoch().subscribe( + (status) => console.log(pool), + (error) => console.error(error), + () => console.log('complete') +) +``` diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 83c74eb..6d21834 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -21,8 +21,10 @@ import { http, parseEventLogs, type Abi, + type Account as AccountType, type Chain, type PublicClient, + type WalletClient, type WatchEventOnLogsParameter, } from 'viem' import { Account } from './Account.js' @@ -32,7 +34,7 @@ import type { HexString } from './types/index.js' import type { CentrifugeQueryOptions, Query } from './types/query.js' import type { OperationStatus, Signer, TransactionCallbackParams } from './types/transaction.js' import { makeThenable, shareReplayWithDelayedReset } from './utils/rx.js' -import { doTransaction } from './utils/transaction.js' +import { doTransaction, isLocalAccount } from './utils/transaction.js' export type Config = { environment: 'mainnet' | 'demo' | 'dev' @@ -120,6 +122,9 @@ export class Centrifuge { return this._query(null, () => of(new Account(this, address, chainId ?? this.config.defaultChain))) } + /** + * Returns an observable of all events on a given chain. + */ events(chainId?: number) { const cid = chainId ?? this.config.defaultChain return this._query( @@ -142,6 +147,9 @@ export class Centrifuge { ).pipe(filter((logs) => logs.length > 0)) } + /** + * Returns an observable of events on a given chain, filtered by name(s) and address(es). + */ filteredEvents(address: string | string[], abi: Abi | Abi[], eventName: string | string[], chainId?: number) { const addresses = (Array.isArray(address) ? address : [address]).map((a) => a.toLowerCase()) const eventNames = Array.isArray(eventName) ? eventName : [eventName] @@ -169,7 +177,6 @@ export class Centrifuge { }, body: JSON.stringify({ query, variables }), selector: async (res) => { - console.log('fetched subquery') const { data, errors } = await res.json() if (errors?.length) { throw errors @@ -220,7 +227,6 @@ export class Centrifuge { function get() { const sharedSubject = new Subject>() function createShared() { - console.log('createShared', keys) const $shared = observableCallback().pipe( keys ? shareReplayWithDelayedReset({ @@ -273,21 +279,25 @@ export class Centrifuge { if (!signer) throw new Error('Signer not set') const publicClient = self.getClient(targetChainId)! - const bareWalletClient = createWalletClient({ transport: custom(signer) }) + const chain = self.getChainConfig(targetChainId) + const walletClient = ( + isLocalAccount(signer) + ? createWalletClient({ account: signer, chain, transport: http() }) + : createWalletClient({ chain, transport: custom(signer) }) + ) as WalletClient - const [address] = await bareWalletClient.getAddresses() + const [address] = await walletClient.getAddresses() if (!address) throw new Error('No account selected') + if (!walletClient.account) { + walletClient.account = { address, type: 'json-rpc' } + } - const chain = self.getChainConfig(targetChainId) - const selectedChain = await bareWalletClient.getChainId() + const selectedChain = await walletClient.getChainId() if (selectedChain !== targetChainId) { yield { type: 'SwitchingChain', chainId: targetChainId } - await bareWalletClient.switchChain({ id: targetChainId }) + await walletClient.switchChain({ id: targetChainId }) } - // Recreate the wallet client with the account and chain - const walletClient = createWalletClient({ account: address, chain, transport: custom(signer) }) - const transaction = callback({ signingAddress: address, chain, diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 7841ff6..4a841c8 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -1,4 +1,4 @@ -import type { Account, Chain, PublicClient, TransactionReceipt, WalletClient } from 'viem' +import type { Account, Chain, LocalAccount, PublicClient, TransactionReceipt, WalletClient } from 'viem' import type { HexString } from './index.js' export type OperationStatusType = @@ -51,7 +51,7 @@ export type EIP1193ProviderLike = { // deprecated, but neccessary for eth-permit send(...args: any): Promise } -export type Signer = EIP1193ProviderLike +export type Signer = EIP1193ProviderLike | LocalAccount export type TransactionCallbackParams = { signingAddress: HexString diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index b08437b..be56a25 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -1,5 +1,5 @@ import { signERC2612Permit } from 'eth-permit' -import type { Account, Chain, PublicClient, WalletClient } from 'viem' +import type { Account, Chain, LocalAccount, PublicClient, WalletClient } from 'viem' import type { HexString } from '../types/index.js' import type { OperationStatus, Signer } from '../types/transaction.js' @@ -62,3 +62,7 @@ export async function signPermit( const permit = await signERC2612Permit(signer, domainOrCurrency, signingAddress, spender, amount.toString(), deadline) return permit as Permit } + +export function isLocalAccount(signer: Signer): signer is LocalAccount { + return 'type' in signer && signer.type === 'local' +} From e8baced5dcfd47bdb4f395b0df5578007bbddb1a Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Tue, 22 Oct 2024 15:24:40 +0200 Subject: [PATCH 11/53] fixes and comments --- README.md | 4 +- src/Account.ts | 177 +++++++++++++++++++++++++++++++++++---- src/Centrifuge.ts | 83 +++++++++++++++--- src/types/transaction.ts | 2 - src/utils/rx.ts | 2 +- src/utils/transaction.ts | 37 +++++--- 6 files changed, 260 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index c7863d9..c500e09 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ centrifuge.setSigner(signer) With this you can call transaction methods. Similar to queries they can be awaited to get their final result, or subscribed to get get status updates. ```js -const pool = centrifuge.pool('1') +const pool = await centrifuge.pool('1') try { const status = await pool.closeEpoch() console.log(status) @@ -71,7 +71,7 @@ try { ``` ```js -const pool = centrifuge.pool('1') +const pool = await centrifuge.pool('1') const subscription = pool.closeEpoch().subscribe( (status) => console.log(pool), (error) => console.error(error), diff --git a/src/Account.ts b/src/Account.ts index 78d0401..7cd45d5 100644 --- a/src/Account.ts +++ b/src/Account.ts @@ -1,11 +1,11 @@ -import { defer, switchMap } from 'rxjs' +import { concat, defer, first, switchMap, type ObservableInput } from 'rxjs' import { ABI } from './abi/index.js' import type { Centrifuge } from './Centrifuge.js' import { NULL_ADDRESS } from './constants.js' import { Entity } from './Entity.js' import type { HexString } from './types/index.js' import { repeatOnEvents } from './utils/rx.js' -import { doSignMessage, doTransaction, signPermit } from './utils/transaction.js' +import { doSignMessage, doTransaction, getTransactionObservable, signPermit } from './utils/transaction.js' const tUSD = '0x8503b4452Bf6238cC76CdbEE223b46d7196b1c93' @@ -51,7 +51,7 @@ export class Account extends Entity { transfer(to: HexString, amount: bigint) { return this._transact( 'Transfer', - async ({ walletClient }) => + ({ walletClient }) => walletClient.writeContract({ address: tUSD, abi: ABI.Currency, @@ -82,28 +82,60 @@ export class Account extends Entity { } transfer3(to: HexString, amount: bigint) { - return this._transact(async function* ({ walletClient, publicClient, chainId, signingAddress, signer }) { - const permit = yield* doSignMessage('Sign Permit', () => { - return signPermit(walletClient, signer, chainId, signingAddress, tUSD, NULL_ADDRESS, amount) - }) - console.log('permit', permit) - yield* doTransaction('Transfer', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - ) - }, this.chainId) + return this._transact( + ({ walletClient, publicClient }) => + this.balances().pipe( + first(), + switchMap((balance) => { + const needsApproval = true + + let $approval: ObservableInput | null = null + if (needsApproval) { + $approval = getTransactionObservable('Approve', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'approve', + args: [tUSD, amount], + }) + ) + } + + const $transfer = getTransactionObservable('Transfer', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + ) + + return $approval ? concat($approval, $transfer) : $transfer + }) + ), + this.chainId + ) } transfer4(to: HexString, amount: bigint) { return this._transact( ({ walletClient, publicClient }) => this.balances().pipe( + first(), switchMap(async function* (balance) { - console.log('balance', balance) + const needsApproval = true + + if (needsApproval) { + yield* doTransaction('Approve', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'approve', + args: [tUSD, amount], + }) + ) + } + yield* doTransaction('Transfer', publicClient, () => walletClient.writeContract({ address: tUSD, @@ -117,4 +149,113 @@ export class Account extends Entity { this.chainId ) } + + transfer5(to: HexString, amount: bigint) { + return this._transact( + ({ walletClient, publicClient, signer, chainId, signingAddress }) => + this.balances().pipe( + first(), + switchMap((balance) => { + const needsApproval = true + const supportsPermit = true + + let $approval: ObservableInput | null = null + let permit: any = null + if (needsApproval) { + if (supportsPermit) { + $approval = doSignMessage('Sign Permit', async () => { + permit = await signPermit(walletClient, signer, chainId, signingAddress, tUSD, NULL_ADDRESS, amount) + return permit + }) + } else { + $approval = doTransaction('Approve', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'approve', + args: [tUSD, amount], + }) + ) + } + } + + const $transfer = defer(() => { + if (permit) { + return doTransaction('Transfer', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + ) + } + return doTransaction('Transfer', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + ) + }) + + return $approval ? concat($approval, $transfer) : $transfer + }) + ), + this.chainId + ) + } + + transfer6(to: HexString, amount: bigint) { + return this._transact( + ({ walletClient, publicClient, signer, chainId, signingAddress }) => + this.balances().pipe( + first(), + switchMap(async function* (balance) { + const needsApproval = true + const supportsPermit = true + + let permit: any = null + if (needsApproval) { + if (supportsPermit) { + permit = yield* doSignMessage('Sign Permit', () => + signPermit(walletClient, signer, chainId, signingAddress, tUSD, NULL_ADDRESS, amount) + ) + } else { + yield* doTransaction('Approve', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'approve', + args: [tUSD, amount], + }) + ) + } + } + + if (permit) { + yield* doTransaction('Transfer', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + ) + } else { + yield* doTransaction('Transfer', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + ) + } + }) + ), + this.chainId + ) + } } diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 6d21834..687830e 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -231,7 +231,7 @@ export class Centrifuge { keys ? shareReplayWithDelayedReset({ bufferSize: (options?.cache ?? true) ? 1 : 0, - resetDelay: (options?.observableCacheTime ?? 60) * 1000, + resetDelay: (options?.cache === false ? 0 : (options?.observableCacheTime ?? 60)) * 1000, windowTime: (options?.valueCacheTime ?? Infinity) * 1000, }) : identity @@ -253,6 +253,64 @@ export class Centrifuge { return keys ? this.#memoizeWith(keys, get) : get() } + /** + * Executes one or more transactions on a given chain. + * When subscribed to, it emits status updates as it progresses. + * When awaited, it returns the final confirmed if successful. + * Will additionally prompt the user to switch chains if they're not on the correct chain. + * + * @example + * + * Execute a single transaction + * + * ```ts + * const tx = this._transact( + * 'Transfer', + * ({ walletClient }) => + * walletClient.writeContract({ + * address: '0xabc...123', + * abi: ABI.Currency, + * functionName: 'transfer', + * args: ['0xdef...456', 1000000n], + * }), + * 1 + * ) + * tx.subscribe(status => console.log(status)) + * + * // Results in something like the following values being emitted (assuming the user wasn't connected to mainnet): + * // { type: 'SwitchingChain', chainId: 1 } + * // { type: 'SigningTransaction', title: 'Transfer' } + * // { type: 'TransactionPending', title: 'Transfer', hash: '0x123...abc' } + * // { type: 'TransactionConfirmed', title: 'Transfer', hash: '0x123...abc', receipt: { ... } } + * ``` + * + * Execute multiple transactions + * + * ```ts + * const tx = this._transact(async function* ({ walletClient, publicClient, chainId, signingAddress, signer }) { + * const permit = yield* doSignMessage('Sign Permit', () => { + * return signPermit(walletClient, signer, chainId, signingAddress, '0xabc...123', '0xdef...456', 1000000n) + * }) + * yield* doTransaction('Invest', publicClient, () => + * walletClient.writeContract({ + * address: '0xdef...456', + * abi: ABI.LiquidityPool, + * functionName: 'requestDepositWithPermit', + * args: [1000000n, permit], + * }) + * ) + * }, 1) + * tx.subscribe(status => console.log(status)) + * + * // Results in something like the following values being emitted (assuming the user was on the right chain): + * // { type: 'SigningMessage', title: 'Sign Permit' } + * // { type: 'SignedMessage', title: 'Sign Permit', signed: { ... } } + * // { type: 'SigningTransaction', title: 'Invest' } + * // { type: 'TransactionPending', title: 'Invest', hash: '0x123...abc' } + * // { type: 'TransactionConfirmed', title: 'Invest', hash: '0x123...abc', receipt: { ... } } + * + * @internal + */ _transact( title: string, transactionCallback: (params: TransactionCallbackParams) => Promise | Observable, @@ -280,24 +338,25 @@ export class Centrifuge { const publicClient = self.getClient(targetChainId)! const chain = self.getChainConfig(targetChainId) - const walletClient = ( - isLocalAccount(signer) - ? createWalletClient({ account: signer, chain, transport: http() }) - : createWalletClient({ chain, transport: custom(signer) }) - ) as WalletClient + const bareWalletClient = isLocalAccount(signer) + ? createWalletClient({ account: signer, chain, transport: http() }) + : createWalletClient({ transport: custom(signer) }) - const [address] = await walletClient.getAddresses() + const [address] = await bareWalletClient.getAddresses() if (!address) throw new Error('No account selected') - if (!walletClient.account) { - walletClient.account = { address, type: 'json-rpc' } - } - const selectedChain = await walletClient.getChainId() + const selectedChain = await bareWalletClient.getChainId() if (selectedChain !== targetChainId) { yield { type: 'SwitchingChain', chainId: targetChainId } - await walletClient.switchChain({ id: targetChainId }) + await bareWalletClient.switchChain({ id: targetChainId }) } + // Re-create the wallet client with the correct chain and account + // Saves having to pass `account` and `chain` to every `writeContract` call + const walletClient = isLocalAccount(signer) + ? (bareWalletClient as WalletClient) + : createWalletClient({ account: address, chain, transport: custom(signer) }) + const transaction = callback({ signingAddress: address, chain, diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 4a841c8..2b8f433 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -48,8 +48,6 @@ export type OperationStatus = export type EIP1193ProviderLike = { request(...args: any): Promise - // deprecated, but neccessary for eth-permit - send(...args: any): Promise } export type Signer = EIP1193ProviderLike | LocalAccount diff --git a/src/utils/rx.ts b/src/utils/rx.ts index 13ac412..91e70e1 100644 --- a/src/utils/rx.ts +++ b/src/utils/rx.ts @@ -14,7 +14,7 @@ export function shareReplayWithDelayedReset(config?: { connector: () => (bufferSize === 0 ? new Subject() : new ReplaySubject(bufferSize, windowTime)), resetOnError: true, resetOnComplete: false, - resetOnRefCountZero: isFinite(resetDelay) ? () => timer(resetDelay) : false, + resetOnRefCountZero: resetDelay === 0 ? true : isFinite(resetDelay) ? () => timer(resetDelay) : false, }) } diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index be56a25..3e2d17e 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -1,4 +1,5 @@ import { signERC2612Permit } from 'eth-permit' +import { concatWith, defer, Observable, of } from 'rxjs' import type { Account, Chain, LocalAccount, PublicClient, WalletClient } from 'viem' import type { HexString } from '../types/index.js' import type { OperationStatus, Signer } from '../types/transaction.js' @@ -10,7 +11,32 @@ export async function* doTransaction( ): AsyncGenerator { yield { type: 'SigningTransaction', title } const hash = await transactionCallback() - yield* waitForTransaction(title, publicClient, hash) + yield { type: 'TransactionPending', title, hash } + const receipt = await publicClient.waitForTransactionReceipt({ hash }) + yield { type: 'TransactionConfirmed', title, hash, receipt } +} + +export function getTransactionObservable( + title: string, + publicClient: PublicClient, + transactionCallback: () => Promise +): Observable { + let hash: HexString | null = null + let receipt: any = null + return of({ type: 'SigningTransaction', title } as const).pipe( + concatWith( + defer(async () => { + hash = await transactionCallback() + return { type: 'TransactionPending', title, hash: hash! } as const + }) + ), + concatWith( + defer(async () => { + receipt = await publicClient.waitForTransactionReceipt({ hash: hash! }) + return { type: 'TransactionConfirmed', title, hash: hash!, receipt } as const + }) + ) + ) } export async function* doSignMessage( @@ -23,15 +49,6 @@ export async function* doSignMessage( return message } -export async function* waitForTransaction( - title: string, - publicClient: PublicClient, - hash: HexString -): AsyncGenerator { - yield { type: 'TransactionPending', title, hash } - const receipt = await publicClient.waitForTransactionReceipt({ hash }) - yield { type: 'TransactionConfirmed', title, hash, receipt } -} export type Permit = { deadline: number | string From 15283038a9a872029c7fc2f0c3eba81d4e931f16 Mon Sep 17 00:00:00 2001 From: sophian Date: Tue, 22 Oct 2024 13:24:29 -0400 Subject: [PATCH 12/53] Map rpc urls to chain id --- src/Centrifuge.ts | 13 +++++++------ src/tests/Centrifuge.test.ts | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index abdd1ed..f04c003 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -38,8 +38,9 @@ import { doTransaction, isLocalAccount } from './utils/transaction.js' export type Config = { environment: 'mainnet' | 'demo' | 'dev' - rpcUrl: string + rpcUrls?: Record subqueryUrl: string + } type DerivedConfig = Config & { defaultChain: number @@ -51,21 +52,18 @@ const envConfig = { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8ed99a9a115349bbbc01dcf3a24edc96', - rpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/8ed99a9a115349bbbc01dcf3a24edc96', defaultChain: 1, }, demo: { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', - rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/8cd8e043ee8d4001b97a1c37e08fd9dd', defaultChain: 11155111, }, dev: { subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', - rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/8cd8e043ee8d4001b97a1c37e08fd9dd', defaultChain: 11155111, }, } @@ -105,16 +103,19 @@ export class Centrifuge { ...defaultConfig, subqueryUrl: defaultConfigForEnv.subqueryUrl, defaultChain: defaultConfigForEnv.defaultChain, - rpcUrl: defaultConfigForEnv.rpcUrl, ...config, } Object.freeze(this.#config) chains .filter((chain) => (this.#config.environment === 'mainnet' ? !chain.testnet : chain.testnet)) .forEach((chain) => { + const rpcUrl = this.#config.rpcUrls?.[`${chain.id}`] ?? undefined + if (!rpcUrl) { + console.warn(`No rpcUrl defined for chain ${chain.id}. Using public RPC endpoint.`) + } this.#clients.set( chain.id, - createPublicClient({ chain, transport: http(this.#config.rpcUrl ?? undefined), batch: { multicall: true } }) + createPublicClient({ chain, transport: http(rpcUrl), batch: { multicall: true } }) ) }) } diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 4584967..07b848c 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -7,10 +7,12 @@ describe('Centrifuge', () => { before(async () => { centrifuge = new Centrifuge({ environment: 'demo', - rpcUrl: 'https://virtual.sepolia.rpc.tenderly.co/ce7949c5-e956-4913-93bf-83b171163bdb', + rpcUrls: { + 11155111: 'https://virtual.sepolia.rpc.tenderly.co/ce7949c5-e956-4913-93bf-83b171163bdb', + }, }); }); - it("should be connected to mainnet", async () => { + it("should be connected to sepolia", async () => { const client = centrifuge.getClient(); expect(client?.chain.id).to.equal(11155111); const chains = centrifuge.chains From 7ad72e0e4dc4cec5e43dff374e2045eb5a01d625 Mon Sep 17 00:00:00 2001 From: sophian Date: Tue, 22 Oct 2024 17:27:26 -0400 Subject: [PATCH 13/53] Create tenderly virtual network for test suite --- .gitignore | 2 + package.json | 8 +- src/Centrifuge.ts | 3 +- src/tests/Centrifuge.test.ts | 49 ++++++---- src/tests/env.example | 3 + src/tests/tenderly.ts | 98 ++++++++++++++++++++ yarn.lock | 175 ++++++++++++++++++++++++++++++++++- 7 files changed, 314 insertions(+), 24 deletions(-) create mode 100644 src/tests/env.example create mode 100644 src/tests/tenderly.ts diff --git a/.gitignore b/.gitignore index 46b93b1..98ece02 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ yarn-error.log !.yarn/plugins !.yarn/sdks !.yarn/versions + +.env diff --git a/package.json b/package.json index 09a39ae..a04384e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "dev": "tsc -w --importHelpers", "build": "tsc --importHelpers", "prepare": "yarn build", - "test": "mocha --loader=ts-node/esm 'src/**/*.test.ts'" + "test": "mocha --loader=ts-node/esm --exit 'src/**/*.test.ts'", + "anvil": "viem-anvil" }, "dependencies": { "eth-permit": "^0.2.3", @@ -34,13 +35,16 @@ "devDependencies": { "@types/chai": "^5.0.0", "@types/mocha": "^10.0.9", + "@types/node": "^22.7.8", + "@viem/anvil": "^0.0.10", "chai": "^5.1.1", + "dotenv": "^16.4.5", "eslint": "^9.12.0", "globals": "^15.11.0", "mocha": "^10.7.3", "npm-run-all": "4.1.5", - "ts-node": "^10.9.2", "prettier": "^3.3.3", + "ts-node": "^10.9.2", "typescript": "~5.6.3", "typescript-eslint": "^8.8.1", "viem": "^2.21.25" diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index f04c003..b7d1119 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -40,7 +40,6 @@ export type Config = { environment: 'mainnet' | 'demo' | 'dev' rpcUrls?: Record subqueryUrl: string - } type DerivedConfig = Config & { defaultChain: number @@ -110,7 +109,7 @@ export class Centrifuge { .filter((chain) => (this.#config.environment === 'mainnet' ? !chain.testnet : chain.testnet)) .forEach((chain) => { const rpcUrl = this.#config.rpcUrls?.[`${chain.id}`] ?? undefined - if (!rpcUrl) { + if (!rpcUrl) { console.warn(`No rpcUrl defined for chain ${chain.id}. Using public RPC endpoint.`) } this.#clients.set( diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 07b848c..86f16c9 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -1,30 +1,43 @@ -import { expect } from 'chai'; -import { Centrifuge } from '../Centrifuge.js'; +import { expect } from 'chai' +import { Centrifuge } from '../Centrifuge.js' +import { forkTenderlyNetwork, deleteTenderlyRpcEndpoint } from './tenderly.js' describe('Centrifuge', () => { let centrifuge: Centrifuge + let vnetId: string before(async () => { + const fork = await forkTenderlyNetwork(11155111) + vnetId = fork.vnetId + console.log('Created tenderly fork with id', vnetId) centrifuge = new Centrifuge({ environment: 'demo', rpcUrls: { - 11155111: 'https://virtual.sepolia.rpc.tenderly.co/ce7949c5-e956-4913-93bf-83b171163bdb', + 11155111: fork.forkedRpcUrl, }, - }); - }); - it("should be connected to sepolia", async () => { - const client = centrifuge.getClient(); - expect(client?.chain.id).to.equal(11155111); + }) + }) + after(async () => { + const deleted = await deleteTenderlyRpcEndpoint(vnetId) + if (deleted) { + console.log('deleted tenderly rpc endpoint with id', vnetId) + } else { + console.log('failed to delete tenderly rpc endpoint with id', vnetId) + } + }) + it('should be connected to sepolia', async () => { + const client = centrifuge.getClient() + expect(client?.chain.id).to.equal(11155111) const chains = centrifuge.chains - expect(chains).to.include(11155111); - }); + expect(chains).to.include(11155111) + }) it('should fetch account and balances', async () => { - const account = await centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f'); - const balances = await account.balances(); - expect(balances).to.exist; - }); + const account = await centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f') + const balances = await account.balances() + expect(balances).to.exist + }) it('should fetch a pool by id', async () => { - const pool = await centrifuge.pool('4139607887'); - expect(pool).to.exist; - }); -}); \ No newline at end of file + const pool = await centrifuge.pool('4139607887') + expect(pool).to.exist + }) +}) diff --git a/src/tests/env.example b/src/tests/env.example new file mode 100644 index 0000000..efda148 --- /dev/null +++ b/src/tests/env.example @@ -0,0 +1,3 @@ +TENDERLY_ACCESS_KEY= +PROJECT_SLUG= +ACCOUNT_SLUG= \ No newline at end of file diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts new file mode 100644 index 0000000..becd39d --- /dev/null +++ b/src/tests/tenderly.ts @@ -0,0 +1,98 @@ +import dotenv from 'dotenv' +dotenv.config({ path: './src/tests/.env' }) + +type TenderlyVirtualNetwork = { + id: string + slug: string + display_name: string + status: string + fork_config: { + network_id: number + block_number: string + } + virtual_network_config: { + chain_config: { + chain_id: number + } + accounts: { + address: string + }[] + } + rpcs: { + url: string + name: string + }[] +} + +type TenderlyError = { + error: { + id: string + slug: string + message: string + } +} + +const TENDERLY_API = 'https://api.tenderly.co/api/v1' +const PROJECT_SLUG = process.env.PROJECT_SLUG +const ACCOUNT_SLUG = process.env.ACCOUNT_SLUG +const TENDERLY_ACCESS_KEY = process.env.TENDERLY_ACCESS_KEY as string + +export const forkTenderlyNetwork = async (chainId: number) => { + try { + const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets` + + const response = await fetch(tenderlyApi, { + method: 'POST', + headers: { + 'X-Access-Key': TENDERLY_ACCESS_KEY, + }, + body: JSON.stringify({ + slug: 'centrifuge-sepolia-fork', + display_name: 'Centrifuge Sepolia Fork', + fork_config: { + network_id: chainId, + block_number: '6924285', + }, + virtual_network_config: { + chain_config: { + chain_id: chainId, + }, + }, + sync_state_config: { + enabled: false, + }, + explorer_page_config: { + enabled: false, + verification_visibility: 'bytecode', + }, + }), + }) + + const virtualNetwork: TenderlyVirtualNetwork | TenderlyError = await response.json() + if ('error' in virtualNetwork) { + throw new Error(JSON.stringify(virtualNetwork.error)) + } + const forkedRpcUrl = virtualNetwork.rpcs.find((rpc) => rpc.name === 'Public RPC')?.url + if (!forkedRpcUrl) { + throw new Error('Failed to find forked RPC URL') + } + return { forkedRpcUrl, vnetId: virtualNetwork.id } + } catch (error) { + throw error + } +} + +export const deleteTenderlyRpcEndpoint = async (vnetId: string) => { + try { + const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets/${vnetId}` + const response = await fetch(tenderlyApi, { + method: 'DELETE', + headers: { + 'X-Access-Key': TENDERLY_ACCESS_KEY as string, + }, + }) + return response.status === 204 + } catch (error) { + throw error + } +} diff --git a/yarn.lock b/yarn.lock index 23d96e1..5790e09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,7 +18,10 @@ __metadata: dependencies: "@types/chai": "npm:^5.0.0" "@types/mocha": "npm:^10.0.9" + "@types/node": "npm:^22.7.8" + "@viem/anvil": "npm:^0.0.10" chai: "npm:^5.1.1" + dotenv: "npm:^16.4.5" eslint: "npm:^9.12.0" eth-permit: "npm:^0.2.3" globals: "npm:^15.11.0" @@ -345,6 +348,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.7.8": + version: 22.7.8 + resolution: "@types/node@npm:22.7.8" + dependencies: + undici-types: "npm:~6.19.2" + checksum: 10c0/3d3b3a2ec5a57ca4fd37b34dce415620993ca5f87cea2c728ffe73aa31446dbfe19c53171c478447bd7d78011ef4845a46ab2f0dc38e699cc75b3d100a60c690 + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:8.11.0": version: 8.11.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.11.0" @@ -461,6 +473,18 @@ __metadata: languageName: node linkType: hard +"@viem/anvil@npm:^0.0.10": + version: 0.0.10 + resolution: "@viem/anvil@npm:0.0.10" + dependencies: + execa: "npm:^7.1.1" + get-port: "npm:^6.1.2" + http-proxy: "npm:^1.18.1" + ws: "npm:^8.13.0" + checksum: 10c0/2b9cdef15e9280fa5c5fe876be8854cfd53d7454978681b059f2dc8ac10e97dedfde76cc5e207f725915527183dc6439145377335d914d469516c3b6cf1a206e + languageName: node + linkType: hard + "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -893,7 +917,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -1006,6 +1030,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.4.5": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 10c0/48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -1307,6 +1338,30 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^4.0.0": + version: 4.0.7 + resolution: "eventemitter3@npm:4.0.7" + checksum: 10c0/5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b + languageName: node + linkType: hard + +"execa@npm:^7.1.1": + version: 7.2.0 + resolution: "execa@npm:7.2.0" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^6.0.1" + human-signals: "npm:^4.3.0" + is-stream: "npm:^3.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^5.1.0" + onetime: "npm:^6.0.0" + signal-exit: "npm:^3.0.7" + strip-final-newline: "npm:^3.0.0" + checksum: 10c0/098cd6a1bc26d509e5402c43f4971736450b84d058391820c6f237aeec6436963e006fd8423c9722f148c53da86aa50045929c7278b5522197dff802d10f9885 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -1411,6 +1466,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.0.0": + version: 1.15.9 + resolution: "follow-redirects@npm:1.15.9" + peerDependenciesMeta: + debug: + optional: true + checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -1520,6 +1585,20 @@ __metadata: languageName: node linkType: hard +"get-port@npm:^6.1.2": + version: 6.1.2 + resolution: "get-port@npm:6.1.2" + checksum: 10c0/cac5f0c600691aed72fdcfacd394b8046080b5208898c3a6b9d10f999466297f162d7907bc6ecbc62d109a904dab7af7cdc0d7933ce2bcecfc5c1fedf7fcfab1 + languageName: node + linkType: hard + +"get-stream@npm:^6.0.1": + version: 6.0.1 + resolution: "get-stream@npm:6.0.1" + checksum: 10c0/49825d57d3fd6964228e6200a58169464b8e8970489b3acdc24906c782fb7f01f9f56f8e6653c4a50713771d6658f7cfe051e5eb8c12e334138c9c918b296341 + languageName: node + linkType: hard + "get-symbol-description@npm:^1.0.2": version: 1.0.2 resolution: "get-symbol-description@npm:1.0.2" @@ -1720,6 +1799,17 @@ __metadata: languageName: node linkType: hard +"http-proxy@npm:^1.18.1": + version: 1.18.1 + resolution: "http-proxy@npm:1.18.1" + dependencies: + eventemitter3: "npm:^4.0.0" + follow-redirects: "npm:^1.0.0" + requires-port: "npm:^1.0.0" + checksum: 10c0/148dfa700a03fb421e383aaaf88ac1d94521dfc34072f6c59770528c65250983c2e4ec996f2f03aa9f3fe46cd1270a593126068319311e3e8d9e610a37533e94 + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1": version: 7.0.5 resolution: "https-proxy-agent@npm:7.0.5" @@ -1730,6 +1820,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^4.3.0": + version: 4.3.1 + resolution: "human-signals@npm:4.3.1" + checksum: 10c0/40498b33fe139f5cc4ef5d2f95eb1803d6318ac1b1c63eaf14eeed5484d26332c828de4a5a05676b6c83d7b9e57727c59addb4b1dea19cb8d71e83689e5b336c + languageName: node + linkType: hard + "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -1966,6 +2063,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "is-stream@npm:3.0.0" + checksum: 10c0/eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8 + languageName: node + linkType: hard + "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -2203,6 +2307,13 @@ __metadata: languageName: node linkType: hard +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5 + languageName: node + linkType: hard + "merge2@npm:^1.3.0": version: 1.4.1 resolution: "merge2@npm:1.4.1" @@ -2220,6 +2331,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: 10c0/de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -2470,6 +2588,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^5.1.0": + version: 5.3.0 + resolution: "npm-run-path@npm:5.3.0" + dependencies: + path-key: "npm:^4.0.0" + checksum: 10c0/124df74820c40c2eb9a8612a254ea1d557ddfab1581c3e751f825e3e366d9f00b0d76a3c94ecd8398e7f3eee193018622677e95816e8491f0797b21e30b2deba + languageName: node + linkType: hard + "object-inspect@npm:^1.13.1": version: 1.13.2 resolution: "object-inspect@npm:1.13.2" @@ -2505,6 +2632,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: "npm:^4.0.0" + checksum: 10c0/4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -2593,6 +2729,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 10c0/794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3 + languageName: node + linkType: hard + "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -2751,6 +2894,13 @@ __metadata: languageName: node linkType: hard +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: 10c0/b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -2957,6 +3107,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^3.0.7": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -3135,6 +3292,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: 10c0/a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce + languageName: node + linkType: hard + "strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -3367,6 +3531,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -3552,7 +3723,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.18.0": +"ws@npm:8.18.0, ws@npm:^8.13.0": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: From c7159bda489b2fb737f2156414891c40ebbcd8cb Mon Sep 17 00:00:00 2001 From: sophian Date: Tue, 22 Oct 2024 17:38:09 -0400 Subject: [PATCH 14/53] Refactor for easier reuse --- src/tests/Centrifuge.test.ts | 15 +++-- src/tests/tenderly.ts | 110 +++++++++++++++++++---------------- 2 files changed, 67 insertions(+), 58 deletions(-) diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 86f16c9..fa88d07 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -1,15 +1,14 @@ import { expect } from 'chai' import { Centrifuge } from '../Centrifuge.js' -import { forkTenderlyNetwork, deleteTenderlyRpcEndpoint } from './tenderly.js' +import { TenderlyFork } from './tenderly.js' describe('Centrifuge', () => { let centrifuge: Centrifuge - let vnetId: string + let tenderlyFork: TenderlyFork before(async () => { - const fork = await forkTenderlyNetwork(11155111) - vnetId = fork.vnetId - console.log('Created tenderly fork with id', vnetId) + tenderlyFork = new TenderlyFork(11155111) + const fork = await tenderlyFork.forkTenderlyNetwork() centrifuge = new Centrifuge({ environment: 'demo', rpcUrls: { @@ -18,11 +17,11 @@ describe('Centrifuge', () => { }) }) after(async () => { - const deleted = await deleteTenderlyRpcEndpoint(vnetId) + const deleted = await tenderlyFork.deleteTenderlyRpcEndpoint() if (deleted) { - console.log('deleted tenderly rpc endpoint with id', vnetId) + console.log('deleted tenderly rpc endpoint with id', tenderlyFork.vnetId) } else { - console.log('failed to delete tenderly rpc endpoint with id', vnetId) + console.log('failed to delete tenderly rpc endpoint with id', tenderlyFork.vnetId) } }) it('should be connected to sepolia', async () => { diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts index becd39d..42e90ba 100644 --- a/src/tests/tenderly.ts +++ b/src/tests/tenderly.ts @@ -37,62 +37,72 @@ const PROJECT_SLUG = process.env.PROJECT_SLUG const ACCOUNT_SLUG = process.env.ACCOUNT_SLUG const TENDERLY_ACCESS_KEY = process.env.TENDERLY_ACCESS_KEY as string -export const forkTenderlyNetwork = async (chainId: number) => { - try { - const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets` +export class TenderlyFork { + #chainId: number + vnetId?: string + rpcUrl?: string + constructor(chainId: number) { + this.#chainId = chainId + } + + forkTenderlyNetwork = async () => { + try { + const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets` - const response = await fetch(tenderlyApi, { - method: 'POST', - headers: { - 'X-Access-Key': TENDERLY_ACCESS_KEY, - }, - body: JSON.stringify({ - slug: 'centrifuge-sepolia-fork', - display_name: 'Centrifuge Sepolia Fork', - fork_config: { - network_id: chainId, - block_number: '6924285', + const response = await fetch(tenderlyApi, { + method: 'POST', + headers: { + 'X-Access-Key': TENDERLY_ACCESS_KEY, }, - virtual_network_config: { - chain_config: { - chain_id: chainId, + body: JSON.stringify({ + slug: 'centrifuge-sepolia-fork', + display_name: 'Centrifuge Sepolia Fork', + fork_config: { + network_id: this.#chainId, + block_number: '6924285', }, - }, - sync_state_config: { - enabled: false, - }, - explorer_page_config: { - enabled: false, - verification_visibility: 'bytecode', - }, - }), - }) + virtual_network_config: { + chain_config: { + chain_id: this.#chainId, + }, + }, + sync_state_config: { + enabled: false, + }, + explorer_page_config: { + enabled: false, + verification_visibility: 'bytecode', + }, + }), + }) - const virtualNetwork: TenderlyVirtualNetwork | TenderlyError = await response.json() - if ('error' in virtualNetwork) { - throw new Error(JSON.stringify(virtualNetwork.error)) - } - const forkedRpcUrl = virtualNetwork.rpcs.find((rpc) => rpc.name === 'Public RPC')?.url - if (!forkedRpcUrl) { - throw new Error('Failed to find forked RPC URL') + const virtualNetwork: TenderlyVirtualNetwork | TenderlyError = await response.json() + if ('error' in virtualNetwork) { + throw new Error(JSON.stringify(virtualNetwork.error)) + } + const forkedRpcUrl = virtualNetwork.rpcs.find((rpc) => rpc.name === 'Public RPC')?.url + if (!forkedRpcUrl) { + throw new Error('Failed to find forked RPC URL') + } + this.vnetId = virtualNetwork.id + return { forkedRpcUrl } + } catch (error) { + throw error } - return { forkedRpcUrl, vnetId: virtualNetwork.id } - } catch (error) { - throw error } -} -export const deleteTenderlyRpcEndpoint = async (vnetId: string) => { - try { - const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets/${vnetId}` - const response = await fetch(tenderlyApi, { - method: 'DELETE', - headers: { - 'X-Access-Key': TENDERLY_ACCESS_KEY as string, - }, - }) - return response.status === 204 - } catch (error) { - throw error + deleteTenderlyRpcEndpoint = async () => { + try { + const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets/${this.vnetId}` + const response = await fetch(tenderlyApi, { + method: 'DELETE', + headers: { + 'X-Access-Key': TENDERLY_ACCESS_KEY as string, + }, + }) + return response.status === 204 + } catch (error) { + throw error + } } } From 26affb5a5a429b49d2461ae9fd1f53f1f1b80b17 Mon Sep 17 00:00:00 2001 From: sophian Date: Tue, 22 Oct 2024 17:45:46 -0400 Subject: [PATCH 15/53] Remove unused package --- package.json | 4 +- yarn.lock | 150 +-------------------------------------------------- 2 files changed, 3 insertions(+), 151 deletions(-) diff --git a/package.json b/package.json index a04384e..6c7f863 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,7 @@ "dev": "tsc -w --importHelpers", "build": "tsc --importHelpers", "prepare": "yarn build", - "test": "mocha --loader=ts-node/esm --exit 'src/**/*.test.ts'", - "anvil": "viem-anvil" + "test": "mocha --loader=ts-node/esm --exit 'src/**/*.test.ts'" }, "dependencies": { "eth-permit": "^0.2.3", @@ -36,7 +35,6 @@ "@types/chai": "^5.0.0", "@types/mocha": "^10.0.9", "@types/node": "^22.7.8", - "@viem/anvil": "^0.0.10", "chai": "^5.1.1", "dotenv": "^16.4.5", "eslint": "^9.12.0", diff --git a/yarn.lock b/yarn.lock index 5790e09..c009e4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,7 +19,6 @@ __metadata: "@types/chai": "npm:^5.0.0" "@types/mocha": "npm:^10.0.9" "@types/node": "npm:^22.7.8" - "@viem/anvil": "npm:^0.0.10" chai: "npm:^5.1.1" dotenv: "npm:^16.4.5" eslint: "npm:^9.12.0" @@ -473,18 +472,6 @@ __metadata: languageName: node linkType: hard -"@viem/anvil@npm:^0.0.10": - version: 0.0.10 - resolution: "@viem/anvil@npm:0.0.10" - dependencies: - execa: "npm:^7.1.1" - get-port: "npm:^6.1.2" - http-proxy: "npm:^1.18.1" - ws: "npm:^8.13.0" - checksum: 10c0/2b9cdef15e9280fa5c5fe876be8854cfd53d7454978681b059f2dc8ac10e97dedfde76cc5e207f725915527183dc6439145377335d914d469516c3b6cf1a206e - languageName: node - linkType: hard - "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -917,7 +904,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -1338,30 +1325,6 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^4.0.0": - version: 4.0.7 - resolution: "eventemitter3@npm:4.0.7" - checksum: 10c0/5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b - languageName: node - linkType: hard - -"execa@npm:^7.1.1": - version: 7.2.0 - resolution: "execa@npm:7.2.0" - dependencies: - cross-spawn: "npm:^7.0.3" - get-stream: "npm:^6.0.1" - human-signals: "npm:^4.3.0" - is-stream: "npm:^3.0.0" - merge-stream: "npm:^2.0.0" - npm-run-path: "npm:^5.1.0" - onetime: "npm:^6.0.0" - signal-exit: "npm:^3.0.7" - strip-final-newline: "npm:^3.0.0" - checksum: 10c0/098cd6a1bc26d509e5402c43f4971736450b84d058391820c6f237aeec6436963e006fd8423c9722f148c53da86aa50045929c7278b5522197dff802d10f9885 - languageName: node - linkType: hard - "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -1466,16 +1429,6 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0": - version: 1.15.9 - resolution: "follow-redirects@npm:1.15.9" - peerDependenciesMeta: - debug: - optional: true - checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f - languageName: node - linkType: hard - "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -1585,20 +1538,6 @@ __metadata: languageName: node linkType: hard -"get-port@npm:^6.1.2": - version: 6.1.2 - resolution: "get-port@npm:6.1.2" - checksum: 10c0/cac5f0c600691aed72fdcfacd394b8046080b5208898c3a6b9d10f999466297f162d7907bc6ecbc62d109a904dab7af7cdc0d7933ce2bcecfc5c1fedf7fcfab1 - languageName: node - linkType: hard - -"get-stream@npm:^6.0.1": - version: 6.0.1 - resolution: "get-stream@npm:6.0.1" - checksum: 10c0/49825d57d3fd6964228e6200a58169464b8e8970489b3acdc24906c782fb7f01f9f56f8e6653c4a50713771d6658f7cfe051e5eb8c12e334138c9c918b296341 - languageName: node - linkType: hard - "get-symbol-description@npm:^1.0.2": version: 1.0.2 resolution: "get-symbol-description@npm:1.0.2" @@ -1799,17 +1738,6 @@ __metadata: languageName: node linkType: hard -"http-proxy@npm:^1.18.1": - version: 1.18.1 - resolution: "http-proxy@npm:1.18.1" - dependencies: - eventemitter3: "npm:^4.0.0" - follow-redirects: "npm:^1.0.0" - requires-port: "npm:^1.0.0" - checksum: 10c0/148dfa700a03fb421e383aaaf88ac1d94521dfc34072f6c59770528c65250983c2e4ec996f2f03aa9f3fe46cd1270a593126068319311e3e8d9e610a37533e94 - languageName: node - linkType: hard - "https-proxy-agent@npm:^7.0.1": version: 7.0.5 resolution: "https-proxy-agent@npm:7.0.5" @@ -1820,13 +1748,6 @@ __metadata: languageName: node linkType: hard -"human-signals@npm:^4.3.0": - version: 4.3.1 - resolution: "human-signals@npm:4.3.1" - checksum: 10c0/40498b33fe139f5cc4ef5d2f95eb1803d6318ac1b1c63eaf14eeed5484d26332c828de4a5a05676b6c83d7b9e57727c59addb4b1dea19cb8d71e83689e5b336c - languageName: node - linkType: hard - "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -2063,13 +1984,6 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "is-stream@npm:3.0.0" - checksum: 10c0/eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8 - languageName: node - linkType: hard - "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -2307,13 +2221,6 @@ __metadata: languageName: node linkType: hard -"merge-stream@npm:^2.0.0": - version: 2.0.0 - resolution: "merge-stream@npm:2.0.0" - checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5 - languageName: node - linkType: hard - "merge2@npm:^1.3.0": version: 1.4.1 resolution: "merge2@npm:1.4.1" @@ -2331,13 +2238,6 @@ __metadata: languageName: node linkType: hard -"mimic-fn@npm:^4.0.0": - version: 4.0.0 - resolution: "mimic-fn@npm:4.0.0" - checksum: 10c0/de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf - languageName: node - linkType: hard - "minimatch@npm:^3.0.4, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -2588,15 +2488,6 @@ __metadata: languageName: node linkType: hard -"npm-run-path@npm:^5.1.0": - version: 5.3.0 - resolution: "npm-run-path@npm:5.3.0" - dependencies: - path-key: "npm:^4.0.0" - checksum: 10c0/124df74820c40c2eb9a8612a254ea1d557ddfab1581c3e751f825e3e366d9f00b0d76a3c94ecd8398e7f3eee193018622677e95816e8491f0797b21e30b2deba - languageName: node - linkType: hard - "object-inspect@npm:^1.13.1": version: 1.13.2 resolution: "object-inspect@npm:1.13.2" @@ -2632,15 +2523,6 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^6.0.0": - version: 6.0.0 - resolution: "onetime@npm:6.0.0" - dependencies: - mimic-fn: "npm:^4.0.0" - checksum: 10c0/4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c - languageName: node - linkType: hard - "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -2729,13 +2611,6 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^4.0.0": - version: 4.0.0 - resolution: "path-key@npm:4.0.0" - checksum: 10c0/794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3 - languageName: node - linkType: hard - "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -2894,13 +2769,6 @@ __metadata: languageName: node linkType: hard -"requires-port@npm:^1.0.0": - version: 1.0.0 - resolution: "requires-port@npm:1.0.0" - checksum: 10c0/b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267 - languageName: node - linkType: hard - "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -3107,13 +2975,6 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.7": - version: 3.0.7 - resolution: "signal-exit@npm:3.0.7" - checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 - languageName: node - linkType: hard - "signal-exit@npm:^4.0.1": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -3292,13 +3153,6 @@ __metadata: languageName: node linkType: hard -"strip-final-newline@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-final-newline@npm:3.0.0" - checksum: 10c0/a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce - languageName: node - linkType: hard - "strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -3723,7 +3577,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.18.0, ws@npm:^8.13.0": +"ws@npm:8.18.0": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: From 8a433b4ecdcfa17a8d794aee2f5988784e8a58b2 Mon Sep 17 00:00:00 2001 From: sophian Date: Wed, 23 Oct 2024 12:14:53 -0400 Subject: [PATCH 16/53] Add readme --- src/tests/README.md | 25 +++++++++++++++++++++++++ src/tests/tenderly.ts | 1 - 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/tests/README.md diff --git a/src/tests/README.md b/src/tests/README.md new file mode 100644 index 0000000..d63d4ff --- /dev/null +++ b/src/tests/README.md @@ -0,0 +1,25 @@ +## Testing with Mocha and Tenderly + +Tenderly allows us to create a local Ethereum network that is compatible with Ethereum Mainnet that we can use to run our tests against. A new fork will be created for each test suite. + +### Local setup + +1. Visit [Tenderly](https://dashboard.tenderly.co/) and create an account if you don't have one. + +2. Create a new project. + +3. Settings is where you will find the values for the environment variables `TENDERLY_ACCESS_KEY` and `TENDERLY_ACCOUNT_SLUG` and `TENDERLY_PROJECT_SLUG` + +4. Copy `env.example` file to `.env` and add the values you found in the previous step. + +Install dependencies: + +```bash +yarn +``` + +Run the tests: + +```bash +yarn test +``` diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts index 42e90ba..a4b7753 100644 --- a/src/tests/tenderly.ts +++ b/src/tests/tenderly.ts @@ -40,7 +40,6 @@ const TENDERLY_ACCESS_KEY = process.env.TENDERLY_ACCESS_KEY as string export class TenderlyFork { #chainId: number vnetId?: string - rpcUrl?: string constructor(chainId: number) { this.#chainId = chainId } From c1f54f2b2784125a89cb1bcd3cc0dd92f77db9da Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Thu, 24 Oct 2024 14:34:34 +0200 Subject: [PATCH 17/53] split transact function --- src/Account.ts | 66 +++++++++++++++++-- src/Centrifuge.ts | 160 +++++++++++++++++++++++++++++++++++++--------- src/Entity.ts | 2 + src/utils/rx.ts | 2 +- 4 files changed, 194 insertions(+), 36 deletions(-) diff --git a/src/Account.ts b/src/Account.ts index 7cd45d5..5a5337e 100644 --- a/src/Account.ts +++ b/src/Account.ts @@ -62,6 +62,27 @@ export class Account extends Entity { ) } + transfer1b(to: HexString, amount: bigint) { + return this._transactSequence(async function* ({ walletClient, publicClient }) { + yield* doTransaction('Transfer', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + ) + yield* doTransaction('Transfer2', publicClient, () => + walletClient.writeContract({ + address: tUSD, + abi: ABI.Currency, + functionName: 'transfer', + args: [to, amount], + }) + ) + }, this.chainId) + } + transfer2(to: HexString, amount: bigint) { return this._transact( 'Transfer', @@ -82,7 +103,7 @@ export class Account extends Entity { } transfer3(to: HexString, amount: bigint) { - return this._transact( + return this._transactSequence( ({ walletClient, publicClient }) => this.balances().pipe( first(), @@ -118,7 +139,7 @@ export class Account extends Entity { } transfer4(to: HexString, amount: bigint) { - return this._transact( + return this._transactSequence( ({ walletClient, publicClient }) => this.balances().pipe( first(), @@ -151,7 +172,7 @@ export class Account extends Entity { } transfer5(to: HexString, amount: bigint) { - return this._transact( + return this._transactSequence( ({ walletClient, publicClient, signer, chainId, signingAddress }) => this.balances().pipe( first(), @@ -208,7 +229,7 @@ export class Account extends Entity { } transfer6(to: HexString, amount: bigint) { - return this._transact( + return this._transactSequence( ({ walletClient, publicClient, signer, chainId, signingAddress }) => this.balances().pipe( first(), @@ -258,4 +279,41 @@ export class Account extends Entity { this.chainId ) } + + // transfer3(to: HexString, amount: bigint) { + // return this._transact(async function* ({ walletClient, publicClient, chainId, signingAddress, signer }) { + // const permit = yield* doSignMessage('Sign Permit', () => { + // return signPermit(walletClient, signer, chainId, signingAddress, tUSD, NULL_ADDRESS, amount) + // }) + // console.log('permit', permit) + // yield* doTransaction('Transfer', publicClient, () => + // walletClient.writeContract({ + // address: tUSD, + // abi: ABI.Currency, + // functionName: 'transfer', + // args: [to, amount], + // }) + // ) + // }, this.chainId) + // } + + // transfer4(to: HexString, amount: bigint) { + // return this._transact( + // ({ walletClient, publicClient }) => + // this.balances().pipe( + // switchMap(async function* (balance) { + // console.log('balance', balance) + // yield* doTransaction('Transfer', publicClient, () => + // walletClient.writeContract({ + // address: tUSD, + // abi: ABI.Currency, + // functionName: 'transfer', + // args: [to, amount], + // }) + // ) + // }) + // ), + // this.chainId + // ) + // } } diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 687830e..ea836c5 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -124,8 +124,10 @@ export class Centrifuge { /** * Returns an observable of all events on a given chain. + * + * @internal */ - events(chainId?: number) { + _events(chainId?: number) { const cid = chainId ?? this.config.defaultChain return this._query( ['events', cid], @@ -149,11 +151,13 @@ export class Centrifuge { /** * Returns an observable of events on a given chain, filtered by name(s) and address(es). + * + * @internal */ - filteredEvents(address: string | string[], abi: Abi | Abi[], eventName: string | string[], chainId?: number) { + _filteredEvents(address: string | string[], abi: Abi | Abi[], eventName: string | string[], chainId?: number) { const addresses = (Array.isArray(address) ? address : [address]).map((a) => a.toLowerCase()) const eventNames = Array.isArray(eventName) ? eventName : [eventName] - return this.events(chainId).pipe( + return this._events(chainId).pipe( map((logs) => { const parsed = parseEventLogs({ abi: abi.flat(), @@ -168,6 +172,9 @@ export class Centrifuge { ) } + /** + * @internal + */ _getSubqueryObservable(query: string, variables?: Record) { return fromFetch(this.config.subqueryUrl, { method: 'POST', @@ -186,6 +193,9 @@ export class Centrifuge { }) } + /** + * @internal + */ _querySubquery( keys: (string | number)[] | null, query: string, @@ -204,7 +214,7 @@ export class Centrifuge { postProcess?: (data: Result) => Return ) { return this._query(keys, () => this._getSubqueryObservable(query, variables).pipe(map(postProcess ?? identity)), { - valueCacheTime: 2, + valueCacheTime: 300, }) } @@ -219,6 +229,87 @@ export class Centrifuge { return result } + /** + * Wraps an observable, memoizing the result based on the keys provided. + * If keys are provided, the observable will be memoized, multicasted, and the last emitted value cached. + * Additional options can be provided to control the caching behavior. + * By default, the observable will keep the last emitted value and pass it immediately to new subscribers. + * When there are no subscribers, the observable resets after a short timeout and purges the cached value. + * + * @example + * + * ```ts + * const address = '0xabc...123' + * const tUSD = '0x456...def' + * const chainId = 1 + * + * // Wrap an observable that continuously emits values + * const query = this._query(['balance', address, tUSD, chainId], () => { + * return defer(() => fetchBalance(address, tUSD, chainId)) + * .pipe( + * repeatOnEvents( + * this, + * { + * address: tUSD, + * abi: ABI.Currency, + * eventName: 'Transfer', + * }, + * chainId + * ) + * ) + * }) + * + * // Logs the current balance and updated balances whenever a transfer event is emitted + * const obs1 = query.subscribe(balance => console.log(balance)) + * + * // Subscribing twice only fetches the balance once and will emit the same value to both subscribers + * const obs2 = query.subscribe(balance => console.log(balance)) + * + * // ... sometime later + * + * // Later subscribers will receive the last emitted value immediately + * const obs3 = query.subscribe(balance => console.log(balance)) + * + * // Awaiting the query will also immediately return the last emitted value or wait for the next value + * const balance = await query + * + * obs1.unsubscribe() + * obs2.unsubscribe() + * obs3.unsubscribe() + * ``` + * + * ```ts + * const address = '0xabc...123' + * const tUSD = '0x456...def' + * const chainId = 1 + * + * // Wrap an observable that only emits one value and then completes + * // + * const query = this._query(['balance', address, tUSD, chainId], () => { + * return defer(() => fetchBalance(address, tUSD, chainId)) + * }, { valueCacheTime: 60 }) + * + * // Logs the current balance and updated balances whenever a new + * const obs1 = query.subscribe(balance => console.log(balance)) + * + * // Subscribing twice only fetches the balance once and will emit the same value to both subscribers + * const obs2 = query.subscribe(balance => console.log(balance)) + * + * // ... sometime later + * + * // Later subscribers will receive the last emitted value immediately + * const obs3 = query.subscribe(balance => console.log(balance)) + * + * // Awaiting the query will also immediately return the last emitted value or wait for the next value + * const balance = await query + * + * obs1.unsubscribe() + * obs2.unsubscribe() + * obs3.unsubscribe() + * ``` + * + * @internal + */ _query( keys: (string | number)[] | null, observableCallback: () => Observable, @@ -254,15 +345,13 @@ export class Centrifuge { } /** - * Executes one or more transactions on a given chain. + * Executes a transaction on a given chain. * When subscribed to, it emits status updates as it progresses. * When awaited, it returns the final confirmed if successful. * Will additionally prompt the user to switch chains if they're not on the correct chain. * * @example * - * Execute a single transaction - * * ```ts * const tx = this._transact( * 'Transfer', @@ -284,7 +373,33 @@ export class Centrifuge { * // { type: 'TransactionConfirmed', title: 'Transfer', hash: '0x123...abc', receipt: { ... } } * ``` * - * Execute multiple transactions + * ```ts + * const finalResult = await this._transact(...) + * console.log(finalResult) // { type: 'TransactionConfirmed', title: 'Transfer', hash: '0x123...abc', receipt: { ... } } + * ``` + * + * @internal + */ + _transact( + title: string, + transactionCallback: (params: TransactionCallbackParams) => Promise | Observable, + chainId?: number + ): Query { + return this._transactSequence(async function* (params) { + const transaction = transactionCallback(params) + yield* doTransaction(title, params.publicClient, () => + 'then' in transaction ? transaction : firstValueFrom(transaction) + ) + }, chainId) + } + + /** + * Executes a sequence of transactions on a given chain. + * When subscribed to, it emits status updates as it progresses. + * When awaited, it returns the final confirmed if successful. + * Will additionally prompt the user to switch chains if they're not on the correct chain. + * + * @example * * ```ts * const tx = this._transact(async function* ({ walletClient, publicClient, chainId, signingAddress, signer }) { @@ -311,26 +426,13 @@ export class Centrifuge { * * @internal */ - _transact( - title: string, - transactionCallback: (params: TransactionCallbackParams) => Promise | Observable, - chainId?: number - ): Query - _transact( + _transactSequence( transactionCallback: ( params: TransactionCallbackParams ) => AsyncGenerator | Observable, chainId?: number - ): Query - _transact(...args: any[]) { - const isSimple = typeof args[0] === 'string' - const title = isSimple ? args[0] : undefined - const callback = (isSimple ? args[1] : args[0]) as ( - params: TransactionCallbackParams - ) => Promise | Observable | AsyncGenerator - const chainId = (isSimple ? args[2] : args[1]) as number + ): Query { const targetChainId = chainId ?? this.config.defaultChain - const self = this async function* transact() { const { signer } = self @@ -347,7 +449,7 @@ export class Centrifuge { const selectedChain = await bareWalletClient.getChainId() if (selectedChain !== targetChainId) { - yield { type: 'SwitchingChain', chainId: targetChainId } + yield { type: 'SwitchingChain', chainId: targetChainId } as const await bareWalletClient.switchChain({ id: targetChainId }) } @@ -357,7 +459,7 @@ export class Centrifuge { ? (bareWalletClient as WalletClient) : createWalletClient({ account: address, chain, transport: custom(signer) }) - const transaction = callback({ + const transaction = transactionCallback({ signingAddress: address, chain, chainId: targetChainId, @@ -365,14 +467,10 @@ export class Centrifuge { walletClient, signer, }) - if (isSimple) { - yield* doTransaction(title, publicClient, () => - 'then' in transaction ? transaction : firstValueFrom(transaction as Observable) - ) - } else if (Symbol.asyncIterator in transaction) { + if (Symbol.asyncIterator in transaction) { yield* transaction } else if (isObservable(transaction)) { - yield transaction as Observable + yield transaction } else { throw new Error('Invalid arguments') } diff --git a/src/Entity.ts b/src/Entity.ts index 887598c..bfeaf82 100644 --- a/src/Entity.ts +++ b/src/Entity.ts @@ -5,12 +5,14 @@ import type { CentrifugeQueryOptions } from './types/query.js' export class Entity { #baseKeys: (string | number)[] _transact: Centrifuge['_transact'] + _transactSequence: Centrifuge['_transactSequence'] constructor( protected _root: Centrifuge, queryKeys: (string | number)[] ) { this.#baseKeys = queryKeys this._transact = this._root._transact.bind(this._root) + this._transactSequence = this._root._transactSequence.bind(this._root) } protected _query( diff --git a/src/utils/rx.ts b/src/utils/rx.ts index 91e70e1..178efaa 100644 --- a/src/utils/rx.ts +++ b/src/utils/rx.ts @@ -30,7 +30,7 @@ export function repeatOnEvents( ): MonoTypeOperatorFunction { return repeat({ delay: () => - centrifuge.filteredEvents(opts.address || [], opts.abi, opts.eventName, chainId).pipe( + centrifuge._filteredEvents(opts.address || [], opts.abi, opts.eventName, chainId).pipe( filter((events) => { return opts.filter ? opts.filter(events) : true }) From 8ae7c1fec0cddf985ca2cbc741e48e10d7dc915f Mon Sep 17 00:00:00 2001 From: sophian Date: Thu, 24 Oct 2024 13:57:17 -0400 Subject: [PATCH 18/53] Increase timeout for api requests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c7f863..a002867 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dev": "tsc -w --importHelpers", "build": "tsc --importHelpers", "prepare": "yarn build", - "test": "mocha --loader=ts-node/esm --exit 'src/**/*.test.ts'" + "test": "mocha --loader=ts-node/esm --exit --timeout 60000 'src/**/*.test.ts'" }, "dependencies": { "eth-permit": "^0.2.3", From 795da27a45cb419393d54dd6a94cc3cbe20f5519 Mon Sep 17 00:00:00 2001 From: sophian Date: Thu, 24 Oct 2024 13:58:12 -0400 Subject: [PATCH 19/53] Make tenderly class more robust and add signing functionality --- src/tests/Centrifuge.test.ts | 36 ++++++--- src/tests/tenderly.ts | 153 ++++++++++++++++++++++++++++++++--- 2 files changed, 169 insertions(+), 20 deletions(-) diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index fa88d07..58fe92d 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -1,28 +1,32 @@ import { expect } from 'chai' import { Centrifuge } from '../Centrifuge.js' import { TenderlyFork } from './tenderly.js' +import { parseEther } from 'viem' describe('Centrifuge', () => { let centrifuge: Centrifuge let tenderlyFork: TenderlyFork before(async () => { - tenderlyFork = new TenderlyFork(11155111) - const fork = await tenderlyFork.forkTenderlyNetwork() + tenderlyFork = new TenderlyFork(11155111, 'dc2f36d8-4c99-461e-8bd0-8e3d99221fc4') + // tenderlyFork = new TenderlyFork(11155111, 'dc2f36d8-4c99-461e-8bd0-8e3d99221fc4') + const fork = await tenderlyFork.forkNetwork() + await Promise.all([ + tenderlyFork.fundAccountEth(tenderlyFork.account.address, parseEther('100')), + tenderlyFork.fundAccountERC20(tenderlyFork.account.address, parseEther('100')), + ]) centrifuge = new Centrifuge({ environment: 'demo', rpcUrls: { - 11155111: fork.forkedRpcUrl, + 11155111: fork.url, + // 11155111: tenderlyFork.getForkedChains(11155111).url, }, }) + + centrifuge.setSigner(tenderlyFork.signer) }) after(async () => { - const deleted = await tenderlyFork.deleteTenderlyRpcEndpoint() - if (deleted) { - console.log('deleted tenderly rpc endpoint with id', tenderlyFork.vnetId) - } else { - console.log('failed to delete tenderly rpc endpoint with id', tenderlyFork.vnetId) - } + return await tenderlyFork.deleteTenderlyRpcEndpoint() }) it('should be connected to sepolia', async () => { const client = centrifuge.getClient() @@ -35,6 +39,20 @@ describe('Centrifuge', () => { const balances = await account.balances() expect(balances).to.exist }) + + it('should make a transfer', async () => { + const account = await centrifuge.account(tenderlyFork.account.address) + const balances = await account.balances() + expect(balances).to.be.greaterThan(0) + // doesn't work yet: ERC20/insufficient-balance + const transfer = await account.transfer('0x423420Ae467df6e90291fd0252c0A8a637C1e03f', parseEther('10')) + if ('receipt' in transfer) { + expect(transfer.receipt.status).to.equal('success') + } else { + throw new Error('Transfer failed') + } + }) + it('should fetch a pool by id', async () => { const pool = await centrifuge.pool('4139607887') expect(pool).to.exist diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts index a4b7753..05c11c6 100644 --- a/src/tests/tenderly.ts +++ b/src/tests/tenderly.ts @@ -1,4 +1,21 @@ import dotenv from 'dotenv' +import { + createPublicClient, + createWalletClient, + http, + rpcSchema, + testActions, + toHex, + type Account, + type Address, + type Chain, + type Hex, + type PublicClient, + type Transport, + type WalletClient, +} from 'viem' +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' +import { mainnet, sepolia } from 'viem/chains' dotenv.config({ path: './src/tests/.env' }) type TenderlyVirtualNetwork = { @@ -32,37 +49,122 @@ type TenderlyError = { } } +type TokenAddress = string +type ReceivingAddress = string +type Amount = `0x${string}` +type CustomRpcSchema = [ + { + Method: 'tenderly_setErc20Balance' + Parameters: [TokenAddress, ReceivingAddress, Amount] + ReturnType: string + }, + { + Method: 'tenderly_setBalance' + Parameters: [ReceivingAddress, Amount] + ReturnType: string + }, +] + +const tUSD = '0x8503b4452Bf6238cC76CdbEE223b46d7196b1c93' + const TENDERLY_API = 'https://api.tenderly.co/api/v1' const PROJECT_SLUG = process.env.PROJECT_SLUG const ACCOUNT_SLUG = process.env.ACCOUNT_SLUG const TENDERLY_ACCESS_KEY = process.env.TENDERLY_ACCESS_KEY as string export class TenderlyFork { - #chainId: number + chainId: number vnetId?: string - constructor(chainId: number) { - this.#chainId = chainId + private _publicClient?: PublicClient + get publicClient(): PublicClient { + if (!this._publicClient) { + this.setPublicClient() + } + return this._publicClient! + } + private _signer?: WalletClient + get signer(): WalletClient { + if (!this._signer) { + this.setSigner() + } + return this._signer! + } + /** + * @returns the account, if no account is set, it will create one + * alternatively, this.acount can set be set with `createAccount` and can receive a private key + */ + private _account?: Account + get account(): Account { + if (!this._account) { + this.createAccount() + } + return this._account! + } + + constructor(chainId: number, vnetId?: string) { + this.chainId = chainId + this.vnetId = vnetId + } + + private setPublicClient() { + const { url, chain } = this.getForkedChains(this.chainId) + this._publicClient = + this._publicClient ?? + createPublicClient({ + chain, + transport: http(url), + rpcSchema: rpcSchema(), + }) + } + + private setSigner(walletAccount?: Account): void { + const { url, chain } = this.getForkedChains(this.chainId) + this._signer = + this._signer ?? + createWalletClient({ + account: walletAccount, + transport: http(url), + chain, + }) } - forkTenderlyNetwork = async () => { + createAccount(privateKey?: Hex) { + const key = privateKey ?? generatePrivateKey() + const walletAccount = privateKeyToAccount(key) + this._account = walletAccount + return walletAccount + } + + getForkedChains(chainId: number): { url: string; chain: Chain } { + if (!this.vnetId) { + throw new Error('Tenderly RPC endpoint not found') + } + const forks = { + 11155111: { url: `https://virtual.sepolia.rpc.tenderly.co/${this.vnetId}`, chain: sepolia }, + 1: { url: `https://virtual.mainnet.rpc.tenderly.co/${this.vnetId}`, chain: mainnet }, + } + return forks[chainId as keyof typeof forks] + } + + async forkNetwork() { try { const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets` - + const timestamp = Date.now() const response = await fetch(tenderlyApi, { method: 'POST', headers: { 'X-Access-Key': TENDERLY_ACCESS_KEY, }, body: JSON.stringify({ - slug: 'centrifuge-sepolia-fork', - display_name: 'Centrifuge Sepolia Fork', + slug: `centrifuge-sepolia-fork-${timestamp}`, + display_name: `Centrifuge Sepolia Fork ${timestamp}`, fork_config: { - network_id: this.#chainId, + network_id: this.chainId, block_number: '6924285', }, virtual_network_config: { chain_config: { - chain_id: this.#chainId, + chain_id: this.chainId, }, }, sync_state_config: { @@ -83,14 +185,15 @@ export class TenderlyFork { if (!forkedRpcUrl) { throw new Error('Failed to find forked RPC URL') } + console.log('Created Tenderly RPC endpoint', virtualNetwork.id) this.vnetId = virtualNetwork.id - return { forkedRpcUrl } + return { url: forkedRpcUrl } } catch (error) { throw error } } - deleteTenderlyRpcEndpoint = async () => { + async deleteTenderlyRpcEndpoint() { try { const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets/${this.vnetId}` const response = await fetch(tenderlyApi, { @@ -99,9 +202,37 @@ export class TenderlyFork { 'X-Access-Key': TENDERLY_ACCESS_KEY as string, }, }) + if (response.status !== 204) { + console.error('Failed to delete Tenderly RPC endpoint', this.vnetId) + throw new Error(JSON.stringify(await response.json())) + } + console.log('Deleted Tenderly RPC endpoint', this.vnetId) return response.status === 204 } catch (error) { + console.error('Failed to delete Tenderly RPC endpoint', this.vnetId) throw error } } + + async fundAccountEth(address: string, amount: bigint) { + const ethResult = await this.publicClient.request({ + jsonrpc: '2.0', + method: 'tenderly_setBalance', + params: [address, toHex(amount)], + id: '1234', + }) + + return ethResult + } + + async fundAccountERC20(address: string, amount: bigint) { + const erc20Result = await this.publicClient.request({ + jsonrpc: '2.0', + method: 'tenderly_setErc20Balance', + params: [tUSD, address, toHex(amount)], + id: '1234', + }) + + return erc20Result + } } From 403c4750b7301d941687b55b1c34dfe5361da18e Mon Sep 17 00:00:00 2001 From: sophian Date: Thu, 24 Oct 2024 16:08:39 -0400 Subject: [PATCH 20/53] Clean up TenderlyFork setup work by adding static create method --- src/tests/Centrifuge.test.ts | 40 +++++------ src/tests/tenderly.ts | 133 +++++++++++++++++++---------------- 2 files changed, 91 insertions(+), 82 deletions(-) diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 58fe92d..5aff1dc 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -1,6 +1,7 @@ import { expect } from 'chai' import { Centrifuge } from '../Centrifuge.js' import { TenderlyFork } from './tenderly.js' +import { sepolia } from 'viem/chains' import { parseEther } from 'viem' describe('Centrifuge', () => { @@ -8,26 +9,19 @@ describe('Centrifuge', () => { let tenderlyFork: TenderlyFork before(async () => { - tenderlyFork = new TenderlyFork(11155111, 'dc2f36d8-4c99-461e-8bd0-8e3d99221fc4') - // tenderlyFork = new TenderlyFork(11155111, 'dc2f36d8-4c99-461e-8bd0-8e3d99221fc4') - const fork = await tenderlyFork.forkNetwork() - await Promise.all([ - tenderlyFork.fundAccountEth(tenderlyFork.account.address, parseEther('100')), - tenderlyFork.fundAccountERC20(tenderlyFork.account.address, parseEther('100')), - ]) + tenderlyFork = await TenderlyFork.create(sepolia) centrifuge = new Centrifuge({ environment: 'demo', rpcUrls: { - 11155111: fork.url, - // 11155111: tenderlyFork.getForkedChains(11155111).url, + 11155111: tenderlyFork.rpcUrl, }, }) - centrifuge.setSigner(tenderlyFork.signer) }) - after(async () => { - return await tenderlyFork.deleteTenderlyRpcEndpoint() - }) + // TODO: don't remove if any test fails + // after(async () => { + // return await tenderlyFork.deleteTenderlyRpcEndpoint() + // }) it('should be connected to sepolia', async () => { const client = centrifuge.getClient() expect(client?.chain.id).to.equal(11155111) @@ -41,16 +35,20 @@ describe('Centrifuge', () => { }) it('should make a transfer', async () => { + await Promise.all([ + tenderlyFork.fundAccountEth(tenderlyFork.account.address, parseEther('100')), + tenderlyFork.fundAccountERC20(tenderlyFork.account.address, parseEther('100')), + ]) const account = await centrifuge.account(tenderlyFork.account.address) const balances = await account.balances() - expect(balances).to.be.greaterThan(0) - // doesn't work yet: ERC20/insufficient-balance - const transfer = await account.transfer('0x423420Ae467df6e90291fd0252c0A8a637C1e03f', parseEther('10')) - if ('receipt' in transfer) { - expect(transfer.receipt.status).to.equal('success') - } else { - throw new Error('Transfer failed') - } + expect(Number(balances)).to.be.greaterThan(0) + // doesn't work: ERC20/insufficient-balance, the tenderly signer does not match the sender of the transfer + // const transfer = await account.transfer('0x423420Ae467df6e90291fd0252c0A8a637C1e03f', parseEther('10')) + // if ('receipt' in transfer) { + // expect(transfer.receipt.status).to.equal('success') + // } else { + // throw new Error('Transfer failed') + // } }) it('should fetch a pool by id', async () => { diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts index 05c11c6..ba7e2ee 100644 --- a/src/tests/tenderly.ts +++ b/src/tests/tenderly.ts @@ -4,7 +4,6 @@ import { createWalletClient, http, rpcSchema, - testActions, toHex, type Account, type Address, @@ -15,7 +14,6 @@ import { type WalletClient, } from 'viem' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' -import { mainnet, sepolia } from 'viem/chains' dotenv.config({ path: './src/tests/.env' }) type TenderlyVirtualNetwork = { @@ -67,65 +65,79 @@ type CustomRpcSchema = [ const tUSD = '0x8503b4452Bf6238cC76CdbEE223b46d7196b1c93' -const TENDERLY_API = 'https://api.tenderly.co/api/v1' +const TENDERLY_API_URL = 'https://api.tenderly.co/api/v1' +const TENDERLY_VNET_URL = 'https://virtual.sepolia.rpc.tenderly.co' const PROJECT_SLUG = process.env.PROJECT_SLUG const ACCOUNT_SLUG = process.env.ACCOUNT_SLUG const TENDERLY_ACCESS_KEY = process.env.TENDERLY_ACCESS_KEY as string export class TenderlyFork { - chainId: number + chain: Chain vnetId?: string + _rpcUrl?: string + forkedNetwork?: TenderlyVirtualNetwork + get rpcUrl(): string { + if (!this._rpcUrl) { + throw new Error('RPC URL is not initialized. Ensure forkNetwork() or TenderlyFork.create() has been called.') + } + return this._rpcUrl + } private _publicClient?: PublicClient get publicClient(): PublicClient { - if (!this._publicClient) { - this.setPublicClient() - } - return this._publicClient! + return this._publicClient ?? this.setPublicClient() } private _signer?: WalletClient get signer(): WalletClient { - if (!this._signer) { - this.setSigner() - } - return this._signer! + return this._signer ?? this.setSigner() } /** - * @returns the account, if no account is set, it will create one - * alternatively, this.acount can set be set with `createAccount` and can receive a private key + * if no account is set, one will be created randomly + * alternatively, this.account can set be set with `createAccount(privateKey)` */ private _account?: Account get account(): Account { - if (!this._account) { - this.createAccount() - } - return this._account! + return this._account ?? this.createAccount() } - constructor(chainId: number, vnetId?: string) { - this.chainId = chainId + constructor(chain: Chain, vnetId?: string, rpcUrl?: string) { + this.chain = chain this.vnetId = vnetId + this._rpcUrl = rpcUrl } - private setPublicClient() { - const { url, chain } = this.getForkedChains(this.chainId) + public static async create(chain: Chain, vnetId?: string): Promise { + let rpcUrl: string + if (vnetId) { + rpcUrl = `${TENDERLY_VNET_URL}/${vnetId}` + return new TenderlyFork(chain, vnetId, rpcUrl) + } else { + const instance = new TenderlyFork(chain) + const { vnetId, url } = await instance.forkNetwork() + return new TenderlyFork(chain, vnetId, url) + } + } + + private setPublicClient(): PublicClient { this._publicClient = this._publicClient ?? createPublicClient({ - chain, - transport: http(url), + chain: this.chain, + transport: http(this.rpcUrl), rpcSchema: rpcSchema(), }) + return this._publicClient! } - private setSigner(walletAccount?: Account): void { - const { url, chain } = this.getForkedChains(this.chainId) + private setSigner(): WalletClient { + const walletAccount = this.account this._signer = this._signer ?? createWalletClient({ account: walletAccount, - transport: http(url), - chain, + transport: http(this.rpcUrl), + chain: this.chain, }) + return this._signer! } createAccount(privateKey?: Hex) { @@ -135,20 +147,9 @@ export class TenderlyFork { return walletAccount } - getForkedChains(chainId: number): { url: string; chain: Chain } { - if (!this.vnetId) { - throw new Error('Tenderly RPC endpoint not found') - } - const forks = { - 11155111: { url: `https://virtual.sepolia.rpc.tenderly.co/${this.vnetId}`, chain: sepolia }, - 1: { url: `https://virtual.mainnet.rpc.tenderly.co/${this.vnetId}`, chain: mainnet }, - } - return forks[chainId as keyof typeof forks] - } - async forkNetwork() { try { - const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets` + const tenderlyApi = `${TENDERLY_API_URL}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets` const timestamp = Date.now() const response = await fetch(tenderlyApi, { method: 'POST', @@ -159,12 +160,12 @@ export class TenderlyFork { slug: `centrifuge-sepolia-fork-${timestamp}`, display_name: `Centrifuge Sepolia Fork ${timestamp}`, fork_config: { - network_id: this.chainId, + network_id: this.chain.id, block_number: '6924285', }, virtual_network_config: { chain_config: { - chain_id: this.chainId, + chain_id: this.chain.id, }, }, sync_state_config: { @@ -181,13 +182,15 @@ export class TenderlyFork { if ('error' in virtualNetwork) { throw new Error(JSON.stringify(virtualNetwork.error)) } - const forkedRpcUrl = virtualNetwork.rpcs.find((rpc) => rpc.name === 'Public RPC')?.url - if (!forkedRpcUrl) { + const forkedRpc = virtualNetwork.rpcs.find((rpc) => rpc.name === 'Admin RPC') + if (!forkedRpc?.url) { throw new Error('Failed to find forked RPC URL') } console.log('Created Tenderly RPC endpoint', virtualNetwork.id) + this.forkedNetwork = virtualNetwork this.vnetId = virtualNetwork.id - return { url: forkedRpcUrl } + this._rpcUrl = forkedRpc.url + return { url: forkedRpc.url, vnetId: virtualNetwork.id } } catch (error) { throw error } @@ -195,7 +198,7 @@ export class TenderlyFork { async deleteTenderlyRpcEndpoint() { try { - const tenderlyApi = `${TENDERLY_API}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets/${this.vnetId}` + const tenderlyApi = `${TENDERLY_API_URL}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets/${this.vnetId}` const response = await fetch(tenderlyApi, { method: 'DELETE', headers: { @@ -215,24 +218,32 @@ export class TenderlyFork { } async fundAccountEth(address: string, amount: bigint) { - const ethResult = await this.publicClient.request({ - jsonrpc: '2.0', - method: 'tenderly_setBalance', - params: [address, toHex(amount)], - id: '1234', - }) + try { + const ethResult = await this.publicClient.request({ + jsonrpc: '2.0', + method: 'tenderly_setBalance', + params: [address, toHex(amount)], + id: '1234', + }) - return ethResult + return ethResult + } catch (error) { + throw error + } } async fundAccountERC20(address: string, amount: bigint) { - const erc20Result = await this.publicClient.request({ - jsonrpc: '2.0', - method: 'tenderly_setErc20Balance', - params: [tUSD, address, toHex(amount)], - id: '1234', - }) - - return erc20Result + try { + const erc20Result = await this.publicClient.request({ + jsonrpc: '2.0', + method: 'tenderly_setErc20Balance', + params: [tUSD, address, toHex(amount)], + id: '1234', + }) + + return erc20Result + } catch (error) { + throw error + } } } From 389a2641f6001cc17bc82fbe637eba85ea57d740 Mon Sep 17 00:00:00 2001 From: sophian Date: Thu, 24 Oct 2024 16:30:45 -0400 Subject: [PATCH 21/53] Update readme --- src/tests/README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/tests/README.md b/src/tests/README.md index d63d4ff..4b1d20a 100644 --- a/src/tests/README.md +++ b/src/tests/README.md @@ -23,3 +23,30 @@ Run the tests: ```bash yarn test ``` + +### Usage + +```ts +// create a new fork +const tenderlyFork = await TenderlyFork.create(sepolia) + +// or use an existing fork +const tenderlyFork = new TenderlyFork(sepolia, '') + +// connect to centrifuge +const centrifuge = new Centrifuge({ + environment: 'demo', + rpcUrls: { + 11155111: tenderlyFork.rpcUrl, + }, +}) + +// set the tenderly signer as the signer for centrifuge +centrifuge.setSigner(tenderlyFork.signer) + +// fund the signer's account on the fork +await tenderlyFork.fundAccountEth(tenderlyFork.account.address, parseEther('100')) + +// fund the signer's account with USDt ERC20 tokens +await tenderlyFork.fundAccountERC20(tenderlyFork.account.address, parseEther('100')) +``` From 08ccd88b2ae88bbb6702a1264b568f27705bc24c Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Fri, 25 Oct 2024 11:05:43 +0200 Subject: [PATCH 22/53] export types --- src/Account.ts | 8 +++++--- src/index.ts | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Account.ts b/src/Account.ts index 5a5337e..c76d9c6 100644 --- a/src/Account.ts +++ b/src/Account.ts @@ -5,7 +5,7 @@ import { NULL_ADDRESS } from './constants.js' import { Entity } from './Entity.js' import type { HexString } from './types/index.js' import { repeatOnEvents } from './utils/rx.js' -import { doSignMessage, doTransaction, getTransactionObservable, signPermit } from './utils/transaction.js' +import { doSignMessage, doTransaction, signPermit } from './utils/transaction.js' const tUSD = '0x8503b4452Bf6238cC76CdbEE223b46d7196b1c93' @@ -63,7 +63,9 @@ export class Account extends Entity { } transfer1b(to: HexString, amount: bigint) { + const self = this return this._transactSequence(async function* ({ walletClient, publicClient }) { + const balance = await self.balances() yield* doTransaction('Transfer', publicClient, () => walletClient.writeContract({ address: tUSD, @@ -112,7 +114,7 @@ export class Account extends Entity { let $approval: ObservableInput | null = null if (needsApproval) { - $approval = getTransactionObservable('Approve', publicClient, () => + $approval = doTransaction('Approve', publicClient, () => walletClient.writeContract({ address: tUSD, abi: ABI.Currency, @@ -122,7 +124,7 @@ export class Account extends Entity { ) } - const $transfer = getTransactionObservable('Transfer', publicClient, () => + const $transfer = doTransaction('Transfer', publicClient, () => walletClient.writeContract({ address: tUSD, abi: ABI.Currency, diff --git a/src/index.ts b/src/index.ts index 1ca9d96..029c49e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,6 @@ import { Centrifuge } from './Centrifuge.js' +export * from './types/index.js' +export * from './types/query.js' +export * from './types/transaction.js' export default Centrifuge From 752b0b4f73742fd3859850c72ec90fa7f769ad1b Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Fri, 25 Oct 2024 15:16:42 +0200 Subject: [PATCH 23/53] transfer test --- eslint.config.js | 2 ++ package.json | 2 +- src/Centrifuge.ts | 4 +-- src/tests/Centrifuge.test.ts | 50 +++++++++++++++++++++++------------- src/tests/tenderly.ts | 13 +++++----- src/utils/transaction.ts | 25 ------------------ tsconfig.json | 18 +++++-------- yarn.lock | 10 ++++---- 8 files changed, 54 insertions(+), 70 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index f57f540..85aa690 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -18,6 +18,8 @@ export default [ 'prefer-template': 'error', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-this-alias': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-unused-expressions': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { diff --git a/package.json b/package.json index a002867..89c5ffa 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@types/chai": "^5.0.0", "@types/mocha": "^10.0.9", "@types/node": "^22.7.8", - "chai": "^5.1.1", + "chai": "^5.1.2", "dotenv": "^16.4.5", "eslint": "^9.12.0", "globals": "^15.11.0", diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 1326ef5..149be64 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -108,7 +108,7 @@ export class Centrifuge { chains .filter((chain) => (this.#config.environment === 'mainnet' ? !chain.testnet : chain.testnet)) .forEach((chain) => { - const rpcUrl = this.#config.rpcUrls?.[`${chain.id}`] ?? undefined + const rpcUrl = this.#config.rpcUrls?.[chain.id] ?? undefined if (!rpcUrl) { console.warn(`No rpcUrl defined for chain ${chain.id}. Using public RPC endpoint.`) } @@ -446,7 +446,7 @@ export class Centrifuge { const publicClient = self.getClient(targetChainId)! const chain = self.getChainConfig(targetChainId) const bareWalletClient = isLocalAccount(signer) - ? createWalletClient({ account: signer, chain, transport: http() }) + ? createWalletClient({ account: signer, chain, transport: http(self.#config.rpcUrls?.[chain.id]) }) : createWalletClient({ transport: custom(signer) }) const [address] = await bareWalletClient.getAddresses() diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 5aff1dc..1446771 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -1,8 +1,10 @@ import { expect } from 'chai' +import { combineLatest, filter, firstValueFrom, last, Observable } from 'rxjs' +import { parseEther } from 'viem' +import { sepolia } from 'viem/chains' import { Centrifuge } from '../Centrifuge.js' +import type { OperationConfirmedStatus } from '../types/transaction.js' import { TenderlyFork } from './tenderly.js' -import { sepolia } from 'viem/chains' -import { parseEther } from 'viem' describe('Centrifuge', () => { let centrifuge: Centrifuge @@ -16,12 +18,12 @@ describe('Centrifuge', () => { 11155111: tenderlyFork.rpcUrl, }, }) - centrifuge.setSigner(tenderlyFork.signer) + centrifuge.setSigner(tenderlyFork.account) }) // TODO: don't remove if any test fails - // after(async () => { - // return await tenderlyFork.deleteTenderlyRpcEndpoint() - // }) + after(async () => { + return await tenderlyFork.deleteTenderlyRpcEndpoint() + }) it('should be connected to sepolia', async () => { const client = centrifuge.getClient() expect(client?.chain.id).to.equal(11155111) @@ -35,20 +37,32 @@ describe('Centrifuge', () => { }) it('should make a transfer', async () => { + const fromAddress = tenderlyFork.account.address + const destAddress = '0x423420Ae467df6e90291fd0252c0A8a637C1e03f' + const transferAmount = 10_000_000n + await Promise.all([ - tenderlyFork.fundAccountEth(tenderlyFork.account.address, parseEther('100')), - tenderlyFork.fundAccountERC20(tenderlyFork.account.address, parseEther('100')), + tenderlyFork.fundAccountEth(fromAddress, parseEther('100')), + tenderlyFork.fundAccountERC20(fromAddress, 100_000_000n), ]) - const account = await centrifuge.account(tenderlyFork.account.address) - const balances = await account.balances() - expect(Number(balances)).to.be.greaterThan(0) - // doesn't work: ERC20/insufficient-balance, the tenderly signer does not match the sender of the transfer - // const transfer = await account.transfer('0x423420Ae467df6e90291fd0252c0A8a637C1e03f', parseEther('10')) - // if ('receipt' in transfer) { - // expect(transfer.receipt.status).to.equal('success') - // } else { - // throw new Error('Transfer failed') - // } + const fromAccount = await centrifuge.account(fromAddress) + const destAccount = await centrifuge.account(destAddress) + const fromBalanceInitial = await fromAccount.balances() + const destBalanceInitial = await destAccount.balances() + + const [transfer, fromBalanceFinal, destBalanceFinal] = await firstValueFrom( + combineLatest([ + fromAccount.transfer(destAddress, transferAmount).pipe(last()) as Observable, + fromAccount.balances().pipe(filter((balance) => balance !== fromBalanceInitial)), + destAccount.balances().pipe(filter((balance) => balance !== destBalanceInitial)), + ]) + ) + + expect(transfer.type).to.equal('TransactionConfirmed') + expect(transfer.title).to.equal('Transfer') + expect(transfer.receipt.status).to.equal('success') + expect(fromBalanceFinal).to.equal(fromBalanceInitial - transferAmount) + expect(destBalanceFinal).to.equal(destBalanceInitial + transferAmount) }) it('should fetch a pool by id', async () => { diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts index ba7e2ee..d6c8095 100644 --- a/src/tests/tenderly.ts +++ b/src/tests/tenderly.ts @@ -6,9 +6,9 @@ import { rpcSchema, toHex, type Account, - type Address, type Chain, type Hex, + type LocalAccount, type PublicClient, type Transport, type WalletClient, @@ -94,8 +94,8 @@ export class TenderlyFork { * if no account is set, one will be created randomly * alternatively, this.account can set be set with `createAccount(privateKey)` */ - private _account?: Account - get account(): Account { + private _account?: LocalAccount + get account(): LocalAccount { return this._account ?? this.createAccount() } @@ -110,11 +110,10 @@ export class TenderlyFork { if (vnetId) { rpcUrl = `${TENDERLY_VNET_URL}/${vnetId}` return new TenderlyFork(chain, vnetId, rpcUrl) - } else { - const instance = new TenderlyFork(chain) - const { vnetId, url } = await instance.forkNetwork() - return new TenderlyFork(chain, vnetId, url) } + const instance = new TenderlyFork(chain) + const { vnetId: _vnetId, url } = await instance.forkNetwork() + return new TenderlyFork(chain, _vnetId, url) } private setPublicClient(): PublicClient { diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index 3e2d17e..c2f7c0e 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -1,5 +1,4 @@ import { signERC2612Permit } from 'eth-permit' -import { concatWith, defer, Observable, of } from 'rxjs' import type { Account, Chain, LocalAccount, PublicClient, WalletClient } from 'viem' import type { HexString } from '../types/index.js' import type { OperationStatus, Signer } from '../types/transaction.js' @@ -16,29 +15,6 @@ export async function* doTransaction( yield { type: 'TransactionConfirmed', title, hash, receipt } } -export function getTransactionObservable( - title: string, - publicClient: PublicClient, - transactionCallback: () => Promise -): Observable { - let hash: HexString | null = null - let receipt: any = null - return of({ type: 'SigningTransaction', title } as const).pipe( - concatWith( - defer(async () => { - hash = await transactionCallback() - return { type: 'TransactionPending', title, hash: hash! } as const - }) - ), - concatWith( - defer(async () => { - receipt = await publicClient.waitForTransactionReceipt({ hash: hash! }) - return { type: 'TransactionConfirmed', title, hash: hash!, receipt } as const - }) - ) - ) -} - export async function* doSignMessage( title: string, transactionCallback: () => Promise @@ -49,7 +25,6 @@ export async function* doSignMessage( return message } - export type Permit = { deadline: number | string r: string diff --git a/tsconfig.json b/tsconfig.json index e79bc27..a58763e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,8 @@ "moduleDetection": "force", "module": "NodeNext", "target": "es2023", - "moduleResolution": "NodeNext", - "esModuleInterop": true, + // "moduleResolution": "NodeNext", + // "esModuleInterop": true, "verbatimModuleSyntax": true, "strict": true, "isolatedModules": true, @@ -16,11 +16,7 @@ "declarationMap": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, - "lib": [ - "es2023", - "dom", - "dom.iterable" - ], + "lib": ["es2023", "dom", "dom.iterable"], // "importHelpers": true, // "rootDir": "./src", "outDir": "dist" @@ -32,8 +28,6 @@ // "noUnusedLocals": true, // "noUnusedParameters": true, }, - "include": [ - "src", - "types" - ] -} \ No newline at end of file + "include": ["src", "src/**/*.d.ts"], + "exclude": ["**/*.test.ts"] +} diff --git a/yarn.lock b/yarn.lock index c009e4a..7a66737 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,7 +19,7 @@ __metadata: "@types/chai": "npm:^5.0.0" "@types/mocha": "npm:^10.0.9" "@types/node": "npm:^22.7.8" - chai: "npm:^5.1.1" + chai: "npm:^5.1.2" dotenv: "npm:^16.4.5" eslint: "npm:^9.12.0" eth-permit: "npm:^0.2.3" @@ -760,16 +760,16 @@ __metadata: languageName: node linkType: hard -"chai@npm:^5.1.1": - version: 5.1.1 - resolution: "chai@npm:5.1.1" +"chai@npm:^5.1.2": + version: 5.1.2 + resolution: "chai@npm:5.1.2" dependencies: assertion-error: "npm:^2.0.1" check-error: "npm:^2.1.1" deep-eql: "npm:^5.0.1" loupe: "npm:^3.1.0" pathval: "npm:^2.0.0" - checksum: 10c0/e7f00e5881e3d5224f08fe63966ed6566bd9fdde175863c7c16dd5240416de9b34c4a0dd925f4fd64ad56256ca6507d32cf6131c49e1db65c62578eb31d4566c + checksum: 10c0/6c04ff8495b6e535df9c1b062b6b094828454e9a3c9493393e55b2f4dbff7aa2a29a4645133cad160fb00a16196c4dc03dc9bb37e1f4ba9df3b5f50d7533a736 languageName: node linkType: hard From e49c6f65f8fdbbc2a98104f7f08fb274de517e40 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Fri, 1 Nov 2024 18:23:37 +0100 Subject: [PATCH 24/53] query tests --- README.md | 11 ++- package.json | 2 + src/Centrifuge.ts | 15 ++-- src/tests/Centrifuge.test.ts | 157 ++++++++++++++++++++++++++++++++++- src/utils/rx.ts | 3 +- yarn.lock | 125 +++++++++++++++++++++++++++- 6 files changed, 297 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c500e09..913766a 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,14 @@ const centrifuge = new Centrifuge() The following config options can be passed on initilization of CentrifugeSDK: -#### `TDB` - -Default value: +- `environment: 'mainnet' | 'demo' | 'dev'` + - Optional + - Default value: `mainnet` +- `rpcUrls: Record` + - Optional + - A object mapping chain ids to RPC URLs +- `subqueryUrl: string` + - Optional ## Queries diff --git a/package.json b/package.json index 89c5ffa..aad2ccf 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/chai": "^5.0.0", "@types/mocha": "^10.0.9", "@types/node": "^22.7.8", + "@types/sinon": "^17.0.3", "chai": "^5.1.2", "dotenv": "^16.4.5", "eslint": "^9.12.0", @@ -42,6 +43,7 @@ "mocha": "^10.7.3", "npm-run-all": "4.1.5", "prettier": "^3.3.3", + "sinon": "^19.0.2", "ts-node": "^10.9.2", "typescript": "~5.6.3", "typescript-eslint": "^8.8.1", diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 149be64..1193f3e 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -324,13 +324,11 @@ export class Centrifuge { const sharedSubject = new Subject>() function createShared() { const $shared = observableCallback().pipe( - keys - ? shareReplayWithDelayedReset({ - bufferSize: (options?.cache ?? true) ? 1 : 0, - resetDelay: (options?.cache === false ? 0 : (options?.observableCacheTime ?? 60)) * 1000, - windowTime: (options?.valueCacheTime ?? Infinity) * 1000, - }) - : identity + shareReplayWithDelayedReset({ + bufferSize: (options?.cache ?? true) ? 1 : 0, + resetDelay: (options?.cache === false ? 0 : (options?.observableCacheTime ?? 60)) * 1000, + windowTime: (options?.valueCacheTime ?? Infinity) * 1000, + }) ) sharedSubject.next($shared) return $shared @@ -338,7 +336,8 @@ export class Centrifuge { const $query = createShared().pipe( // For new subscribers, recreate the shared observable if the previously shared observable has completed - // and no longer has a cached value, which can happen with a finite `valueCacheTime`. + // and no longer has a cached value, which can happen with a a long `observableCacheTime` + // and a finite `valueCacheTime`. defaultIfEmpty(defer(createShared)), // For existing subscribers, merge any newly created shared observable. concatWith(sharedSubject), diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 1446771..b9a5903 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' -import { combineLatest, filter, firstValueFrom, last, Observable } from 'rxjs' +import { combineLatest, defer, filter, firstValueFrom, last, Observable, of, Subject, tap } from 'rxjs' +import sinon from 'sinon' import { parseEther } from 'viem' -import { sepolia } from 'viem/chains' import { Centrifuge } from '../Centrifuge.js' import type { OperationConfirmedStatus } from '../types/transaction.js' import { TenderlyFork } from './tenderly.js' @@ -9,6 +9,7 @@ import { TenderlyFork } from './tenderly.js' describe('Centrifuge', () => { let centrifuge: Centrifuge let tenderlyFork: TenderlyFork + let clock: sinon.SinonFakeTimers before(async () => { tenderlyFork = await TenderlyFork.create(sepolia) @@ -24,7 +25,16 @@ describe('Centrifuge', () => { after(async () => { return await tenderlyFork.deleteTenderlyRpcEndpoint() }) - it('should be connected to sepolia', async () => { + + beforeEach(() => { + clock = sinon.useFakeTimers({ shouldAdvanceTime: true }) + }) + + afterEach(() => { + clock.restore() + }) + + xit('should be connected to sepolia', async () => { const client = centrifuge.getClient() expect(client?.chain.id).to.equal(11155111) const chains = centrifuge.chains @@ -69,4 +79,145 @@ describe('Centrifuge', () => { const pool = await centrifuge.pool('4139607887') expect(pool).to.exist }) + + describe('Queries', () => { + it('should return the first value when awaited', async () => { + const value = await centrifuge._query(null, () => of(1, 2, 3)) + expect(value).to.equal(1) + }) + + it('should memoize the observable by key', async () => { + const object1 = {} + const object2 = {} + let secondObservableCalled = false + const query1 = centrifuge._query(['key'], () => of(object1)) + const query2 = centrifuge._query(['key'], () => of(object2).pipe(tap(() => (secondObservableCalled = true)))) + const value1 = await query1 + const value2 = await query2 + expect(query1).to.equal(query2) + expect(value1).to.equal(value2) + expect(value1).to.equal(object1) + expect(secondObservableCalled).to.equal(false) + }) + + it("should't memoize observables with different keys", async () => { + const object1 = {} + const object2 = {} + const query1 = centrifuge._query(['key', 1], () => of(object1)) + const query2 = centrifuge._query(['key', 2], () => of(object2)) + const value1 = await query1 + const value2 = await query2 + expect(query1).to.not.equal(query2) + expect(value1).to.equal(object1) + expect(value2).to.equal(object2) + }) + + it("should't memoize the observable when no keys are passed", async () => { + const object1 = {} + const object2 = {} + const query1 = centrifuge._query(null, () => of(object1)) + const query2 = centrifuge._query(null, () => of(object2)) + const value1 = await query1 + const value2 = await query2 + expect(query1).to.not.equal(query2) + expect(value1).to.equal(object1) + expect(value2).to.equal(object2) + }) + + it('should cache the latest value by default', async () => { + let subscribedTimes = 0 + const query1 = centrifuge._query(null, () => + defer(() => { + subscribedTimes++ + return lazy(1) + }) + ) + const value1 = await query1 + const value2 = await query1 + const value3 = await firstValueFrom(query1) + expect(subscribedTimes).to.equal(1) + expect(value1).to.equal(1) + expect(value2).to.equal(1) + expect(value3).to.equal(1) + }) + + it("should invalidate the cache when there's no subscribers for a while on an infinite observable", async () => { + const subject = new Subject() + const query1 = centrifuge._query(null, () => subject) + setTimeout(() => subject.next(1), 10) + const value1 = await query1 + setTimeout(() => subject.next(2), 10) + const value2 = await query1 + clock.tick(60_000) + setTimeout(() => subject.next(3), 10) + const value3 = await query1 + expect(value1).to.equal(1) + expect(value2).to.equal(1) + expect(value3).to.equal(3) + }) + + it('should invalidate the cache after a timeout when a finite observable completes', async () => { + let value = 0 + const query1 = centrifuge._query(null, () => defer(() => lazy(++value)), { valueCacheTime: 1 }) + const value1 = await query1 + const value2 = await query1 + clock.tick(60_000) + const value3 = await query1 + expect(value1).to.equal(1) + expect(value2).to.equal(1) + expect(value3).to.equal(2) + }) + + it("shouldn't cache the latest value when `cache` is `false`", async () => { + let value = 0 + const query1 = centrifuge._query( + null, + () => + defer(() => { + value++ + return lazy(value) + }), + { cache: false } + ) + const value1 = await query1 + const value2 = await query1 + const value3 = await firstValueFrom(query1) + expect(value1).to.equal(1) + expect(value2).to.equal(2) + expect(value3).to.equal(3) + }) + + it("shouldn't reset the cache with a longer `observableCacheTime`", async () => { + let value = 0 + const query1 = centrifuge._query(null, () => defer(() => lazy(++value)), { observableCacheTime: Infinity }) + const value1 = await query1 + clock.tick(1_000_000_000) + const value2 = await query1 + expect(value1).to.equal(1) + expect(value2).to.equal(1) + }) + + it('should push new data for new subscribers to old subscribers', async () => { + let value = 0 + const query1 = centrifuge._query(null, () => defer(() => lazy(++value)), { valueCacheTime: 1 }) + let lastValue: number | null = null + const subscription = query1.subscribe((next) => { + console.log('query1 next', next) + lastValue = next + }) + await query1 + clock.tick(500_000) + + console.log('gonna await ') + const value2 = await query1 + console.log('value2', value2) + expect(value2).to.equal(2) + expect(lastValue).to.equal(2) + subscription.unsubscribe() + }) + }) }) + +function lazy(value: T) { + return new Promise((res) => setTimeout(() => res(value), 10)) +} diff --git a/src/utils/rx.ts b/src/utils/rx.ts index 178efaa..d23f5ed 100644 --- a/src/utils/rx.ts +++ b/src/utils/rx.ts @@ -10,11 +10,12 @@ export function shareReplayWithDelayedReset(config?: { resetDelay?: number }): MonoTypeOperatorFunction { const { bufferSize = Infinity, windowTime = Infinity, resetDelay = 1000 } = config ?? {} + const reset = resetDelay === 0 ? true : isFinite(resetDelay) ? () => timer(resetDelay) : false return share({ connector: () => (bufferSize === 0 ? new Subject() : new ReplaySubject(bufferSize, windowTime)), resetOnError: true, resetOnComplete: false, - resetOnRefCountZero: resetDelay === 0 ? true : isFinite(resetDelay) ? () => timer(resetDelay) : false, + resetOnRefCountZero: reset, }) } diff --git a/yarn.lock b/yarn.lock index 7a66737..62a3ff9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,7 @@ __metadata: "@types/chai": "npm:^5.0.0" "@types/mocha": "npm:^10.0.9" "@types/node": "npm:^22.7.8" + "@types/sinon": "npm:^17.0.3" chai: "npm:^5.1.2" dotenv: "npm:^16.4.5" eslint: "npm:^9.12.0" @@ -28,6 +29,7 @@ __metadata: npm-run-all: "npm:4.1.5" prettier: "npm:^3.3.3" rxjs: "npm:^7.8.1" + sinon: "npm:^19.0.2" ts-node: "npm:^10.9.2" typescript: "npm:~5.6.3" typescript-eslint: "npm:^8.8.1" @@ -291,6 +293,42 @@ __metadata: languageName: node linkType: hard +"@sinonjs/commons@npm:^3.0.1": + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" + dependencies: + type-detect: "npm:4.0.8" + checksum: 10c0/1227a7b5bd6c6f9584274db996d7f8cee2c8c350534b9d0141fc662eaf1f292ea0ae3ed19e5e5271c8fd390d27e492ca2803acd31a1978be2cdc6be0da711403 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^13.0.1, @sinonjs/fake-timers@npm:^13.0.2": + version: 13.0.5 + resolution: "@sinonjs/fake-timers@npm:13.0.5" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + checksum: 10c0/a707476efd523d2138ef6bba916c83c4a377a8372ef04fad87499458af9f01afc58f4f245c5fd062793d6d70587309330c6f96947b5bd5697961c18004dc3e26 + languageName: node + linkType: hard + +"@sinonjs/samsam@npm:^8.0.1": + version: 8.0.2 + resolution: "@sinonjs/samsam@npm:8.0.2" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + lodash.get: "npm:^4.4.2" + type-detect: "npm:^4.1.0" + checksum: 10c0/31d74c415040161f2963a202d7f866bedbb5a9b522a74b08a17086c15a75c3ef2893eecebb0c65a7b1603ef4ebdf83fa73cbe384b4cd679944918ed833200443 + languageName: node + linkType: hard + +"@sinonjs/text-encoding@npm:^0.7.3": + version: 0.7.3 + resolution: "@sinonjs/text-encoding@npm:0.7.3" + checksum: 10c0/b112d1e97af7f99fbdc63c7dbcd35d6a60764dfec85cfcfff532e55cce8ecd8453f9fa2139e70aea47142c940fd90cd201d19f370b9a0141700d8a6de3116815 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.11 resolution: "@tsconfig/node10@npm:1.0.11" @@ -356,6 +394,22 @@ __metadata: languageName: node linkType: hard +"@types/sinon@npm:^17.0.3": + version: 17.0.3 + resolution: "@types/sinon@npm:17.0.3" + dependencies: + "@types/sinonjs__fake-timers": "npm:*" + checksum: 10c0/6fc3aa497fd87826375de3dbddc2bf01c281b517c32c05edf95b5ad906382dc221bca01ca9d44fc7d5cb4c768f996f268154e87633a45b3c0b5cddca7ef5e2be + languageName: node + linkType: hard + +"@types/sinonjs__fake-timers@npm:*": + version: 8.1.5 + resolution: "@types/sinonjs__fake-timers@npm:8.1.5" + checksum: 10c0/2b8bdc246365518fc1b08f5720445093cce586183acca19a560be6ef81f824bd9a96c090e462f622af4d206406dadf2033c5daf99a51c1096da6494e5c8dc32e + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:8.11.0": version: 8.11.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.11.0" @@ -1017,6 +1071,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^7.0.0": + version: 7.0.0 + resolution: "diff@npm:7.0.0" + checksum: 10c0/251fd15f85ffdf814cfc35a728d526b8d2ad3de338dcbd011ac6e57c461417090766b28995f8ff733135b5fbc3699c392db1d5e27711ac4e00244768cd1d577b + languageName: node + linkType: hard + "dotenv@npm:^16.4.5": version: 16.4.5 resolution: "dotenv@npm:16.4.5" @@ -2116,6 +2177,13 @@ __metadata: languageName: node linkType: hard +"just-extend@npm:^6.2.0": + version: 6.2.0 + resolution: "just-extend@npm:6.2.0" + checksum: 10c0/d41cbdb6d85b986d4deaf2144d81d4f7266cd408fc95189d046d63f610c2dc486b141aeb6ef319c2d76fe904d45a6bb31f19b098ff0427c35688e0c383fc0511 + languageName: node + linkType: hard + "keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -2156,6 +2224,13 @@ __metadata: languageName: node linkType: hard +"lodash.get@npm:^4.4.2": + version: 4.4.2 + resolution: "lodash.get@npm:4.4.2" + checksum: 10c0/48f40d471a1654397ed41685495acb31498d5ed696185ac8973daef424a749ca0c7871bf7b665d5c14f5cc479394479e0307e781f61d5573831769593411be6e + languageName: node + linkType: hard + "lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" @@ -2417,6 +2492,19 @@ __metadata: languageName: node linkType: hard +"nise@npm:^6.1.1": + version: 6.1.1 + resolution: "nise@npm:6.1.1" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + "@sinonjs/fake-timers": "npm:^13.0.1" + "@sinonjs/text-encoding": "npm:^0.7.3" + just-extend: "npm:^6.2.0" + path-to-regexp: "npm:^8.1.0" + checksum: 10c0/09471adb738dc3be2981cc7815c90879ed6a5a3e162202ca66e12f9a5a0956bea718d0ec2f0c07acc26e3f958481b8fb30c30da76c13620e922f3b9dcd249c50 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 10.2.0 resolution: "node-gyp@npm:10.2.0" @@ -2628,6 +2716,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^8.1.0": + version: 8.2.0 + resolution: "path-to-regexp@npm:8.2.0" + checksum: 10c0/ef7d0a887b603c0a142fad16ccebdcdc42910f0b14830517c724466ad676107476bba2fe9fffd28fd4c141391ccd42ea426f32bb44c2c82ecaefe10c37b90f5a + languageName: node + linkType: hard + "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -2982,6 +3077,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^19.0.2": + version: 19.0.2 + resolution: "sinon@npm:19.0.2" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + "@sinonjs/fake-timers": "npm:^13.0.2" + "@sinonjs/samsam": "npm:^8.0.1" + diff: "npm:^7.0.0" + nise: "npm:^6.1.1" + supports-color: "npm:^7.2.0" + checksum: 10c0/a5d988d55643677e55bbc70c3aa6c9977f8a7cf55d157278ea8e4474d9acbec16d9c44056bd763e4f5988daf0fb75370099cf90105243dbb8742978478d53c40 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -3169,7 +3278,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.1.0": +"supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -3287,6 +3396,20 @@ __metadata: languageName: node linkType: hard +"type-detect@npm:4.0.8": + version: 4.0.8 + resolution: "type-detect@npm:4.0.8" + checksum: 10c0/8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd + languageName: node + linkType: hard + +"type-detect@npm:^4.1.0": + version: 4.1.0 + resolution: "type-detect@npm:4.1.0" + checksum: 10c0/df8157ca3f5d311edc22885abc134e18ff8ffbc93d6a9848af5b682730ca6a5a44499259750197250479c5331a8a75b5537529df5ec410622041650a7f293e2a + languageName: node + linkType: hard + "typed-array-buffer@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-buffer@npm:1.0.2" From 4ea1ec7036804f70c26bac612a5297b7bcaa8d02 Mon Sep 17 00:00:00 2001 From: Sophia Date: Mon, 4 Nov 2024 09:58:11 -0500 Subject: [PATCH 25/53] Extract setup of Centrifuge(Test) and TenderlyFork into Context (#9) * Extract setup of Centrifuge and TenderlyFork into context accessed via withContext * Keep virtual network alive if test fails * Export context directly and introduce debug flag to prevent vn being deleted --- package.json | 2 +- src/tests/Centrifuge.test.ts | 25 +++++++++++--------- src/tests/env.example | 3 ++- src/tests/setup.ts | 44 ++++++++++++++++++++++++++++++++++++ src/tests/tenderly.ts | 2 +- 5 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 src/tests/setup.ts diff --git a/package.json b/package.json index aad2ccf..5e359c4 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dev": "tsc -w --importHelpers", "build": "tsc --importHelpers", "prepare": "yarn build", - "test": "mocha --loader=ts-node/esm --exit --timeout 60000 'src/**/*.test.ts'" + "test": "mocha --loader=ts-node/esm --require $(pwd)/src/tests/setup.ts --exit --timeout 60000 'src/**/*.test.ts'" }, "dependencies": { "eth-permit": "^0.2.3", diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index b9a5903..38e3bbf 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -4,7 +4,9 @@ import sinon from 'sinon' import { parseEther } from 'viem' import { Centrifuge } from '../Centrifuge.js' import type { OperationConfirmedStatus } from '../types/transaction.js' +import { context } from './setup.js' import { TenderlyFork } from './tenderly.js' +import { sepolia } from 'viem/chains' describe('Centrifuge', () => { let centrifuge: Centrifuge @@ -37,26 +39,27 @@ describe('Centrifuge', () => { xit('should be connected to sepolia', async () => { const client = centrifuge.getClient() expect(client?.chain.id).to.equal(11155111) - const chains = centrifuge.chains + const chains = context.centrifuge.chains expect(chains).to.include(11155111) }) - it('should fetch account and balances', async () => { - const account = await centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f') + + it('should fetch account and balances', async function () { + const account = await context.centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f') const balances = await account.balances() expect(balances).to.exist }) - it('should make a transfer', async () => { - const fromAddress = tenderlyFork.account.address + it('should make a transfer', async function () { + const fromAddress = this.context.tenderlyFork.account.address const destAddress = '0x423420Ae467df6e90291fd0252c0A8a637C1e03f' const transferAmount = 10_000_000n await Promise.all([ - tenderlyFork.fundAccountEth(fromAddress, parseEther('100')), - tenderlyFork.fundAccountERC20(fromAddress, 100_000_000n), + context.tenderlyFork.fundAccountEth(fromAddress, parseEther('100')), + context.tenderlyFork.fundAccountERC20(fromAddress, 100_000_000n), ]) - const fromAccount = await centrifuge.account(fromAddress) - const destAccount = await centrifuge.account(destAddress) + const fromAccount = await context.centrifuge.account(fromAddress) + const destAccount = await context.centrifuge.account(destAddress) const fromBalanceInitial = await fromAccount.balances() const destBalanceInitial = await destAccount.balances() @@ -75,8 +78,8 @@ describe('Centrifuge', () => { expect(destBalanceFinal).to.equal(destBalanceInitial + transferAmount) }) - it('should fetch a pool by id', async () => { - const pool = await centrifuge.pool('4139607887') + it('should fetch a pool by id', async function () { + const pool = await context.centrifuge.pool('4139607887') expect(pool).to.exist }) diff --git a/src/tests/env.example b/src/tests/env.example index efda148..e888990 100644 --- a/src/tests/env.example +++ b/src/tests/env.example @@ -1,3 +1,4 @@ TENDERLY_ACCESS_KEY= PROJECT_SLUG= -ACCOUNT_SLUG= \ No newline at end of file +ACCOUNT_SLUG= +DEBUG=false # set to true to keep the tenderly RPC endpoint alive after tests have finished \ No newline at end of file diff --git a/src/tests/setup.ts b/src/tests/setup.ts new file mode 100644 index 0000000..9f45cb4 --- /dev/null +++ b/src/tests/setup.ts @@ -0,0 +1,44 @@ +import { Centrifuge } from '../Centrifuge.js' +import { TenderlyFork } from './tenderly.js' +import { sepolia } from 'viem/chains' + +class TestContext { + public centrifuge!: Centrifuge + public tenderlyFork!: TenderlyFork + public allTestsSucceeded = true + + async initialize() { + this.tenderlyFork = await TenderlyFork.create(sepolia) + this.centrifuge = new Centrifuge({ + environment: 'demo', + rpcUrls: { + 11155111: this.tenderlyFork.rpcUrl, + }, + }) + this.centrifuge.setSigner(this.tenderlyFork.account) + } + + async cleanup() { + if (process.env.DEBUG === 'true') { + console.log('DEBUG is true, RPC endpoint will not be deleted', this.tenderlyFork.rpcUrl) + return + } + if (this.tenderlyFork && this.allTestsSucceeded) { + return this.tenderlyFork.deleteTenderlyRpcEndpoint() + } + console.log('A test has failed, RPC endpoint will not be deleted', this.tenderlyFork.rpcUrl) + } +} + +export const context = new TestContext() + +export const mochaHooks = { + beforeAll: async function (this: Mocha.Context & TestContext) { + this.timeout(30000) // Increase timeout for setup + await context.initialize() + this.context = context as TestContext + }, + afterAll: async function (this: Mocha.Context & TestContext) { + await context.cleanup() + }, +} diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts index d6c8095..d6d03a1 100644 --- a/src/tests/tenderly.ts +++ b/src/tests/tenderly.ts @@ -1,4 +1,3 @@ -import dotenv from 'dotenv' import { createPublicClient, createWalletClient, @@ -14,6 +13,7 @@ import { type WalletClient, } from 'viem' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' +import dotenv from 'dotenv' dotenv.config({ path: './src/tests/.env' }) type TenderlyVirtualNetwork = { From e8e91e3c357c8066efa8d9a094f05f2e85518713 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Wed, 6 Nov 2024 13:53:06 +0100 Subject: [PATCH 26/53] tx test --- src/Account.ts | 1 + src/Centrifuge.ts | 21 ++- src/PoolDomain.ts | 89 +++++++++---- src/abi/index.ts | 16 +-- src/tests/Centrifuge.test.ts | 239 +++++++++++++++++++++++++++-------- src/tests/setup.ts | 6 +- 6 files changed, 275 insertions(+), 97 deletions(-) diff --git a/src/Account.ts b/src/Account.ts index c76d9c6..3205ac4 100644 --- a/src/Account.ts +++ b/src/Account.ts @@ -37,6 +37,7 @@ export class Account extends Entity { abi: ABI.Currency, eventName: 'Transfer', filter: (events) => { + console.log('Transfer Event') return events.some((event) => { return event.args.from === this.accountId || event.args.to === this.accountId }) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 1193f3e..88551d1 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -322,7 +322,7 @@ export class Centrifuge { ): Query { function get() { const sharedSubject = new Subject>() - function createShared() { + function createShared(): Observable { const $shared = observableCallback().pipe( shareReplayWithDelayedReset({ bufferSize: (options?.cache ?? true) ? 1 : 0, @@ -334,10 +334,25 @@ export class Centrifuge { return $shared } + /* + SCENARIO 1: + When the shared inner completes, the inner is reset + When a new subscriber subscribes after the shared inner completes, the shared inner is not re-created + Instead, it re-subscribes to the shared inner, which restarts + This causes existing subscribers to not receive the new values, because `createShared` is not called and thus `sharedSubject.next($shared)` is not called + + SCENARIO 2: + When the shared inner completes, the inner is not reset + In which case, it relies on the `valueCacheTime` to invalidate the cache which is infinite by default + + + What we want is a an Infinite valueCacheTime for infinite observables and a finite cache time for finite observables + + */ + const $query = createShared().pipe( // For new subscribers, recreate the shared observable if the previously shared observable has completed - // and no longer has a cached value, which can happen with a a long `observableCacheTime` - // and a finite `valueCacheTime`. + // and no longer has a cached value, which can happen with a finite `valueCacheTime`. defaultIfEmpty(defer(createShared)), // For existing subscribers, merge any newly created shared observable. concatWith(sharedSubject), diff --git a/src/PoolDomain.ts b/src/PoolDomain.ts index 5f4b80c..a0dc943 100644 --- a/src/PoolDomain.ts +++ b/src/PoolDomain.ts @@ -9,42 +9,40 @@ import type { HexString } from './types/index.js' import { repeatOnEvents } from './utils/rx.js' export class PoolDomain extends Entity { - constructor(centrifuge: Centrifuge, public pool: Pool, public chainId: number) { + constructor( + centrifuge: Centrifuge, + public pool: Pool, + public chainId: number + ) { super(centrifuge, ['pool', pool.id, 'domain', chainId]) } manager() { - return this._root._query( - ['domainManager', this.chainId], - () => - defer(async () => { - const { router } = lpConfig[this.chainId]! - const client = this._root.getClient(this.chainId)! - const gatewayAddress = await getContract({ address: router, abi: ABI.Router, client }).read.gateway!() - const managerAddress = await getContract({ address: gatewayAddress as any, abi: ABI.Gateway, client }).read - .investmentManager!() - console.log('managerAddress', managerAddress) - return managerAddress as HexString - }), - { observableCacheTime: Infinity } + return this._root._query(['domainManager', this.chainId], () => + defer(async () => { + const { router } = lpConfig[this.chainId]! + const client = this._root.getClient(this.chainId)! + const gatewayAddress = await getContract({ address: router, abi: ABI.Router, client }).read.gateway!() + const managerAddress = await getContract({ address: gatewayAddress as any, abi: ABI.Gateway, client }).read + .investmentManager!() + console.log('managerAddress', managerAddress) + return managerAddress as HexString + }) ) } poolManager() { - return this._root._query( - ['domainPoolManager', this.chainId], - () => - this.manager().pipe( - switchMap((manager) => { - return getContract({ - address: manager, - abi: ABI.InvestmentManager, - client: this._root.getClient(this.chainId)!, - }).read.poolManager!() as Promise - }), - tap((poolManager) => console.log('poolManager', poolManager)) - ), - { observableCacheTime: Infinity } + return this._root._query(['domainPoolManager', this.chainId], () => + this.manager().pipe( + switchMap((manager) => { + return getContract({ + address: manager, + abi: ABI.InvestmentManager, + client: this._root.getClient(this.chainId)!, + }).read.poolManager!() as Promise + }), + tap((poolManager) => console.log('poolManager', poolManager)) + ) ) } @@ -79,3 +77,38 @@ export class PoolDomain extends Entity { ) } } + +// repeat({ +// delay: () => +// this._root.filteredEvents(manager, ABI.PoolManager, 'AddPool', this.chainId).pipe( +// filter((events) => { +// return events.some((event) => { +// return event.args.poolId === this.pool.id +// }) +// }) +// ), +// }) + +// repeatOnEvents( +// this._root, +// { +// address: manager, +// abi: ABI.PoolManager, +// eventName: 'AddPool', +// filter: (events) => { +// return events.some((event) => { +// return event.args.poolId === this.pool.id +// }) +// }, +// }, +// this.chainId +// ) + +// type ClassDecorator = ( +// value: Function, +// context: { +// kind: 'class'; +// name: string | undefined; +// addInitializer(initializer: () => void): void; +// } +// ) => Function | void; diff --git a/src/abi/index.ts b/src/abi/index.ts index e03133f..4bb2f43 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -1,12 +1,12 @@ import { parseAbi } from 'viem' -import CentrifugeRouter from './CentrifugeRouter.abi.json' assert { type: 'json' } -import Currency from './Currency.abi.json' assert { type: 'json' } -import Gateway from './Gateway.abi.json' assert { type: 'json' } -import InvestmentManager from './InvestmentManager.abi.json' assert { type: 'json' } -import LiquidityPool from './LiquidityPool.abi.json' assert { type: 'json' } -import Permit from './Permit.abi.json' assert { type: 'json' } -import PoolManager from './PoolManager.abi.json' assert { type: 'json' } -import Router from './Router.abi.json' assert { type: 'json' } +import CentrifugeRouter from './CentrifugeRouter.abi.js' +import Currency from './Currency.abi.js' +import Gateway from './Gateway.abi.js' +import InvestmentManager from './InvestmentManager.abi.js' +import LiquidityPool from './LiquidityPool.abi.js' +import Permit from './Permit.abi.js' +import PoolManager from './PoolManager.abi.js' +import Router from './Router.abi.js' export const ABI = { CentrifugeRouter: parseAbi(CentrifugeRouter), diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 38e3bbf..1a1000c 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -1,33 +1,28 @@ import { expect } from 'chai' -import { combineLatest, defer, filter, firstValueFrom, last, Observable, of, Subject, tap } from 'rxjs' +import { + combineLatest, + defer, + filter, + firstValueFrom, + interval, + map, + Observable, + of, + Subject, + take, + tap, + toArray, +} from 'rxjs' import sinon from 'sinon' -import { parseEther } from 'viem' +import { createClient, custom, parseEther } from 'viem' import { Centrifuge } from '../Centrifuge.js' import type { OperationConfirmedStatus } from '../types/transaction.js' +import { doSignMessage, doTransaction } from '../utils/transaction.js' import { context } from './setup.js' -import { TenderlyFork } from './tenderly.js' -import { sepolia } from 'viem/chains' describe('Centrifuge', () => { - let centrifuge: Centrifuge - let tenderlyFork: TenderlyFork let clock: sinon.SinonFakeTimers - before(async () => { - tenderlyFork = await TenderlyFork.create(sepolia) - centrifuge = new Centrifuge({ - environment: 'demo', - rpcUrls: { - 11155111: tenderlyFork.rpcUrl, - }, - }) - centrifuge.setSigner(tenderlyFork.account) - }) - // TODO: don't remove if any test fails - after(async () => { - return await tenderlyFork.deleteTenderlyRpcEndpoint() - }) - beforeEach(() => { clock = sinon.useFakeTimers({ shouldAdvanceTime: true }) }) @@ -36,14 +31,14 @@ describe('Centrifuge', () => { clock.restore() }) - xit('should be connected to sepolia', async () => { - const client = centrifuge.getClient() + it('should be connected to sepolia', async () => { + const client = context.centrifuge.getClient() expect(client?.chain.id).to.equal(11155111) const chains = context.centrifuge.chains expect(chains).to.include(11155111) }) - it('should fetch account and balances', async function () { + it('should fetch account and balances', async () => { const account = await context.centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f') const balances = await account.balances() expect(balances).to.exist @@ -63,29 +58,29 @@ describe('Centrifuge', () => { const fromBalanceInitial = await fromAccount.balances() const destBalanceInitial = await destAccount.balances() - const [transfer, fromBalanceFinal, destBalanceFinal] = await firstValueFrom( + const [status, fromBalanceFinal, destBalanceFinal] = await firstValueFrom( combineLatest([ - fromAccount.transfer(destAddress, transferAmount).pipe(last()) as Observable, + fromAccount.transfer(destAddress, transferAmount) as Observable, fromAccount.balances().pipe(filter((balance) => balance !== fromBalanceInitial)), destAccount.balances().pipe(filter((balance) => balance !== destBalanceInitial)), ]) ) - expect(transfer.type).to.equal('TransactionConfirmed') - expect(transfer.title).to.equal('Transfer') - expect(transfer.receipt.status).to.equal('success') + expect(status.type).to.equal('TransactionConfirmed') + expect(status.title).to.equal('Transfer') + expect(status.receipt.status).to.equal('success') expect(fromBalanceFinal).to.equal(fromBalanceInitial - transferAmount) expect(destBalanceFinal).to.equal(destBalanceInitial + transferAmount) }) - it('should fetch a pool by id', async function () { + it('should fetch a pool by id', async () => { const pool = await context.centrifuge.pool('4139607887') expect(pool).to.exist }) describe('Queries', () => { it('should return the first value when awaited', async () => { - const value = await centrifuge._query(null, () => of(1, 2, 3)) + const value = await context.centrifuge._query(null, () => of(1, 2, 3)) expect(value).to.equal(1) }) @@ -93,8 +88,10 @@ describe('Centrifuge', () => { const object1 = {} const object2 = {} let secondObservableCalled = false - const query1 = centrifuge._query(['key'], () => of(object1)) - const query2 = centrifuge._query(['key'], () => of(object2).pipe(tap(() => (secondObservableCalled = true)))) + const query1 = context.centrifuge._query(['key'], () => of(object1)) + const query2 = context.centrifuge._query(['key'], () => + of(object2).pipe(tap(() => (secondObservableCalled = true))) + ) const value1 = await query1 const value2 = await query2 expect(query1).to.equal(query2) @@ -106,8 +103,8 @@ describe('Centrifuge', () => { it("should't memoize observables with different keys", async () => { const object1 = {} const object2 = {} - const query1 = centrifuge._query(['key', 1], () => of(object1)) - const query2 = centrifuge._query(['key', 2], () => of(object2)) + const query1 = context.centrifuge._query(['key', 1], () => of(object1)) + const query2 = context.centrifuge._query(['key', 2], () => of(object2)) const value1 = await query1 const value2 = await query2 expect(query1).to.not.equal(query2) @@ -118,8 +115,8 @@ describe('Centrifuge', () => { it("should't memoize the observable when no keys are passed", async () => { const object1 = {} const object2 = {} - const query1 = centrifuge._query(null, () => of(object1)) - const query2 = centrifuge._query(null, () => of(object2)) + const query1 = context.centrifuge._query(null, () => of(object1)) + const query2 = context.centrifuge._query(null, () => of(object2)) const value1 = await query1 const value2 = await query2 expect(query1).to.not.equal(query2) @@ -129,7 +126,7 @@ describe('Centrifuge', () => { it('should cache the latest value by default', async () => { let subscribedTimes = 0 - const query1 = centrifuge._query(null, () => + const query1 = context.centrifuge._query(null, () => defer(() => { subscribedTimes++ return lazy(1) @@ -146,25 +143,28 @@ describe('Centrifuge', () => { it("should invalidate the cache when there's no subscribers for a while on an infinite observable", async () => { const subject = new Subject() - const query1 = centrifuge._query(null, () => subject) + const query1 = context.centrifuge._query(null, () => subject) setTimeout(() => subject.next(1), 10) const value1 = await query1 setTimeout(() => subject.next(2), 10) const value2 = await query1 + clock.tick(10) + const value3 = await query1 clock.tick(60_000) setTimeout(() => subject.next(3), 10) - const value3 = await query1 + const value4 = await query1 expect(value1).to.equal(1) expect(value2).to.equal(1) - expect(value3).to.equal(3) + expect(value3).to.equal(2) + expect(value4).to.equal(3) }) - it('should invalidate the cache after a timeout when a finite observable completes', async () => { + it('should invalidate the cache when a finite observable completes, when given a `valueCacheTime`', async () => { let value = 0 - const query1 = centrifuge._query(null, () => defer(() => lazy(++value)), { valueCacheTime: 1 }) + const query1 = context.centrifuge._query(null, () => defer(() => lazy(++value)), { valueCacheTime: 1 }) const value1 = await query1 const value2 = await query1 - clock.tick(60_000) + clock.tick(1_000) const value3 = await query1 expect(value1).to.equal(1) expect(value2).to.equal(1) @@ -173,7 +173,7 @@ describe('Centrifuge', () => { it("shouldn't cache the latest value when `cache` is `false`", async () => { let value = 0 - const query1 = centrifuge._query( + const query1 = context.centrifuge._query( null, () => defer(() => { @@ -192,7 +192,9 @@ describe('Centrifuge', () => { it("shouldn't reset the cache with a longer `observableCacheTime`", async () => { let value = 0 - const query1 = centrifuge._query(null, () => defer(() => lazy(++value)), { observableCacheTime: Infinity }) + const query1 = context.centrifuge._query(null, () => defer(() => lazy(++value)), { + observableCacheTime: Infinity, + }) const value1 = await query1 clock.tick(1_000_000_000) const value2 = await query1 @@ -202,25 +204,152 @@ describe('Centrifuge', () => { it('should push new data for new subscribers to old subscribers', async () => { let value = 0 - const query1 = centrifuge._query(null, () => defer(() => lazy(++value)), { valueCacheTime: 1 }) + const query1 = context.centrifuge._query(null, () => defer(() => lazy(++value)), { valueCacheTime: 1 }) let lastValue: number | null = null - const subscription = query1.subscribe((next) => { - console.log('query1 next', next) - lastValue = next - }) + const subscription = query1.subscribe((next) => (lastValue = next)) await query1 - clock.tick(500_000) - - console.log('gonna await ') + clock.tick(60_000) const value2 = await query1 - console.log('value2', value2) expect(value2).to.equal(2) expect(lastValue).to.equal(2) subscription.unsubscribe() }) + + it('should cache nested queries', async () => { + const query1 = context.centrifuge._query(['key1'], () => interval(50).pipe(map((i) => i + 1))) + const query2 = context.centrifuge._query(['key2'], () => interval(50).pipe(map((i) => (i + 1) * 2))) + const query3 = context.centrifuge._query(null, () => + combineLatest([query1, query2]).pipe(map(([v1, v2]) => v1 + v2)) + ) + const value1 = await query3 + const value2 = await query3 + clock.tick(50) + const value3 = await query3 + expect(value1).to.equal(3) + expect(value2).to.equal(3) + expect(value3).to.equal(6) + }) + }) + + describe('Transactions', () => { + it('should throw when no account is selected', async () => { + const cent = new Centrifuge({ + environment: 'demo', + }) + cent.setSigner(mockProvider({ accounts: [] })) + const tx = cent._transact('Test', async () => '0x1' as const) + let error + try { + await firstValueFrom(tx) + } catch (e) { + error = e + } + expect(error).to.instanceOf(Error) + }) + + it('should try to switch chains when the signer is connected to a different one', async () => { + const cent = new Centrifuge({ + environment: 'demo', + }) + const signer = mockProvider({ chainId: 1 }) + const spy = sinon.spy(signer, 'request') + cent.setSigner(signer) + const tx = cent._transact('Test', async () => '0x1' as const) + const statuses: any = await firstValueFrom(tx.pipe(take(2), toArray())) + expect(statuses[0]).to.eql({ + type: 'SwitchingChain', + chainId: 11155111, + }) + expect(spy.thirdCall.args[0].method).to.equal('wallet_switchEthereumChain') + expect(Number(spy.thirdCall.args[0].params[0].chainId)).to.equal(11155111) + }) + + it("shouldn't try to switch chains when the signer is connected to the right chain", async () => { + const cent = new Centrifuge({ + environment: 'demo', + }) + cent.setSigner(mockProvider()) + + const tx = cent._transact('Test', async () => '0x1' as const) + const status: any = await firstValueFrom(tx) + expect(status.type).to.equal('SigningTransaction') + expect(status.title).to.equal('Test') + }) + + it('should emit status updates', async () => { + const cent = new Centrifuge({ + environment: 'demo', + }) + cent.setSigner(mockProvider()) + const publicClient: any = createClient({ transport: custom(mockProvider()) }).extend(() => ({ + waitForTransactionReceipt: async () => ({}), + })) + const tx = cent._transactSequence(() => doTransaction('Test', publicClient, async () => '0x1')) + const statuses = await firstValueFrom(tx.pipe(toArray())) + expect(statuses).to.eql([ + { type: 'SigningTransaction', title: 'Test' }, + { type: 'TransactionPending', title: 'Test', hash: '0x1' }, + { + type: 'TransactionConfirmed', + title: 'Test', + hash: '0x1', + receipt: {}, + }, + ]) + }) + + it('should emit status updates for a sequence of transactions', async () => { + const cent = new Centrifuge({ + environment: 'demo', + }) + cent.setSigner(mockProvider()) + const publicClient: any = createClient({ transport: custom(mockProvider()) }).extend(() => ({ + waitForTransactionReceipt: async () => ({}), + })) + const tx = cent._transactSequence(async function* () { + yield* doSignMessage('Sign Permit', async () => '0x1') + yield* doTransaction('Test', publicClient, async () => '0x2') + }) + const statuses = await firstValueFrom(tx.pipe(toArray())) + expect(statuses).to.eql([ + { + type: 'SigningMessage', + title: 'Sign Permit', + }, + { + type: 'SignedMessage', + signed: '0x1', + title: 'Sign Permit', + }, + { type: 'SigningTransaction', title: 'Test' }, + { type: 'TransactionPending', title: 'Test', hash: '0x2' }, + { + type: 'TransactionConfirmed', + title: 'Test', + hash: '0x2', + receipt: {}, + }, + ]) + }) }) }) function lazy(value: T) { return new Promise((res) => setTimeout(() => res(value), 10)) } + +function mockProvider({ chainId = 11155111, accounts = ['0x2'] } = {}) { + return { + async request({ method }: any) { + switch (method) { + case 'eth_accounts': + return accounts + case 'eth_chainId': + return chainId + case 'wallet_switchEthereumChain': + return true + } + throw new Error(`Unknown method ${method}`) + }, + } +} diff --git a/src/tests/setup.ts b/src/tests/setup.ts index 9f45cb4..ca2ee0f 100644 --- a/src/tests/setup.ts +++ b/src/tests/setup.ts @@ -1,6 +1,6 @@ +import { sepolia } from 'viem/chains' import { Centrifuge } from '../Centrifuge.js' import { TenderlyFork } from './tenderly.js' -import { sepolia } from 'viem/chains' class TestContext { public centrifuge!: Centrifuge @@ -33,12 +33,12 @@ class TestContext { export const context = new TestContext() export const mochaHooks = { - beforeAll: async function (this: Mocha.Context & TestContext) { + async beforeAll(this: Mocha.Context & TestContext) { this.timeout(30000) // Increase timeout for setup await context.initialize() this.context = context as TestContext }, - afterAll: async function (this: Mocha.Context & TestContext) { + async afterAll(this: Mocha.Context & TestContext) { await context.cleanup() }, } From e5ba56eb56c35e456b93eca79f0f5a3d956d9465 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Tue, 12 Nov 2024 09:36:01 +0100 Subject: [PATCH 27/53] cleanup --- src/Account.ts | 261 +--------------------------------------- src/Centrifuge.ts | 4 +- src/Pool.ts | 33 +---- src/PoolDomain.ts | 60 ++------- src/abi/Permit.abi.json | 1 - src/abi/index.ts | 2 - src/config/lp.ts | 6 +- src/constants.ts | 1 + src/index.ts | 2 + 9 files changed, 28 insertions(+), 342 deletions(-) delete mode 100644 src/abi/Permit.abi.json diff --git a/src/Account.ts b/src/Account.ts index 3205ac4..d43bf91 100644 --- a/src/Account.ts +++ b/src/Account.ts @@ -1,11 +1,9 @@ -import { concat, defer, first, switchMap, type ObservableInput } from 'rxjs' +import { defer } from 'rxjs' import { ABI } from './abi/index.js' import type { Centrifuge } from './Centrifuge.js' -import { NULL_ADDRESS } from './constants.js' import { Entity } from './Entity.js' import type { HexString } from './types/index.js' import { repeatOnEvents } from './utils/rx.js' -import { doSignMessage, doTransaction, signPermit } from './utils/transaction.js' const tUSD = '0x8503b4452Bf6238cC76CdbEE223b46d7196b1c93' @@ -62,261 +60,4 @@ export class Account extends Entity { this.chainId ) } - - transfer1b(to: HexString, amount: bigint) { - const self = this - return this._transactSequence(async function* ({ walletClient, publicClient }) { - const balance = await self.balances() - yield* doTransaction('Transfer', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - ) - yield* doTransaction('Transfer2', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - ) - }, this.chainId) - } - - transfer2(to: HexString, amount: bigint) { - return this._transact( - 'Transfer', - ({ walletClient }) => - this.balances().pipe( - switchMap((balance) => { - console.log('balance', balance) - return walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - }) - ), - this.chainId - ) - } - - transfer3(to: HexString, amount: bigint) { - return this._transactSequence( - ({ walletClient, publicClient }) => - this.balances().pipe( - first(), - switchMap((balance) => { - const needsApproval = true - - let $approval: ObservableInput | null = null - if (needsApproval) { - $approval = doTransaction('Approve', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'approve', - args: [tUSD, amount], - }) - ) - } - - const $transfer = doTransaction('Transfer', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - ) - - return $approval ? concat($approval, $transfer) : $transfer - }) - ), - this.chainId - ) - } - - transfer4(to: HexString, amount: bigint) { - return this._transactSequence( - ({ walletClient, publicClient }) => - this.balances().pipe( - first(), - switchMap(async function* (balance) { - const needsApproval = true - - if (needsApproval) { - yield* doTransaction('Approve', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'approve', - args: [tUSD, amount], - }) - ) - } - - yield* doTransaction('Transfer', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - ) - }) - ), - this.chainId - ) - } - - transfer5(to: HexString, amount: bigint) { - return this._transactSequence( - ({ walletClient, publicClient, signer, chainId, signingAddress }) => - this.balances().pipe( - first(), - switchMap((balance) => { - const needsApproval = true - const supportsPermit = true - - let $approval: ObservableInput | null = null - let permit: any = null - if (needsApproval) { - if (supportsPermit) { - $approval = doSignMessage('Sign Permit', async () => { - permit = await signPermit(walletClient, signer, chainId, signingAddress, tUSD, NULL_ADDRESS, amount) - return permit - }) - } else { - $approval = doTransaction('Approve', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'approve', - args: [tUSD, amount], - }) - ) - } - } - - const $transfer = defer(() => { - if (permit) { - return doTransaction('Transfer', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - ) - } - return doTransaction('Transfer', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - ) - }) - - return $approval ? concat($approval, $transfer) : $transfer - }) - ), - this.chainId - ) - } - - transfer6(to: HexString, amount: bigint) { - return this._transactSequence( - ({ walletClient, publicClient, signer, chainId, signingAddress }) => - this.balances().pipe( - first(), - switchMap(async function* (balance) { - const needsApproval = true - const supportsPermit = true - - let permit: any = null - if (needsApproval) { - if (supportsPermit) { - permit = yield* doSignMessage('Sign Permit', () => - signPermit(walletClient, signer, chainId, signingAddress, tUSD, NULL_ADDRESS, amount) - ) - } else { - yield* doTransaction('Approve', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'approve', - args: [tUSD, amount], - }) - ) - } - } - - if (permit) { - yield* doTransaction('Transfer', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - ) - } else { - yield* doTransaction('Transfer', publicClient, () => - walletClient.writeContract({ - address: tUSD, - abi: ABI.Currency, - functionName: 'transfer', - args: [to, amount], - }) - ) - } - }) - ), - this.chainId - ) - } - - // transfer3(to: HexString, amount: bigint) { - // return this._transact(async function* ({ walletClient, publicClient, chainId, signingAddress, signer }) { - // const permit = yield* doSignMessage('Sign Permit', () => { - // return signPermit(walletClient, signer, chainId, signingAddress, tUSD, NULL_ADDRESS, amount) - // }) - // console.log('permit', permit) - // yield* doTransaction('Transfer', publicClient, () => - // walletClient.writeContract({ - // address: tUSD, - // abi: ABI.Currency, - // functionName: 'transfer', - // args: [to, amount], - // }) - // ) - // }, this.chainId) - // } - - // transfer4(to: HexString, amount: bigint) { - // return this._transact( - // ({ walletClient, publicClient }) => - // this.balances().pipe( - // switchMap(async function* (balance) { - // console.log('balance', balance) - // yield* doTransaction('Transfer', publicClient, () => - // walletClient.writeContract({ - // address: tUSD, - // abi: ABI.Currency, - // functionName: 'transfer', - // args: [to, amount], - // }) - // ) - // }) - // ), - // this.chainId - // ) - // } } diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 88551d1..ac11283 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -123,8 +123,8 @@ export class Centrifuge { return this._query(null, () => of(new Pool(this, id))) } - account(address: HexString, chainId?: number) { - return this._query(null, () => of(new Account(this, address, chainId ?? this.config.defaultChain))) + account(address: string, chainId?: number) { + return this._query(null, () => of(new Account(this, address as any, chainId ?? this.config.defaultChain))) } /** diff --git a/src/Pool.ts b/src/Pool.ts index 59613d1..2fa28fa 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -4,7 +4,10 @@ import { Entity } from './Entity.js' import { PoolDomain } from './PoolDomain.js' export class Pool extends Entity { - constructor(_root: Centrifuge, public id: string) { + constructor( + _root: Centrifuge, + public id: string + ) { super(_root, ['pool', id]) } @@ -18,28 +21,6 @@ export class Pool extends Entity { }) } - // return this._query(['tranches'], () => { - // return this._root - // ._getSubqueryObservable<{ pool: { tranches: { nodes: { trancheId: string }[] } } }>( - // `query($poolId: String!) { - // pool(id: $poolId) { - // tranches { - // nodes { - // trancheId - // } - // } - // } - // }`, - // { - // poolId: this.id, - // } - // ) - // .pipe( - // map((data) => { - // return data.pool.tranches.nodes.map((node) => node.trancheId) - // }) - // ) - // }) tranches() { return this._root._querySubquery( ['tranches', this.id], @@ -74,11 +55,7 @@ export class Pool extends Entity { }) ) ) - ).pipe( - map((isActive) => { - return domains.filter((_, index) => isActive[index]) - }) - ) + ).pipe(map((isActive) => domains.filter((_, index) => isActive[index]))) }) ) }) diff --git a/src/PoolDomain.ts b/src/PoolDomain.ts index a0dc943..3d496f6 100644 --- a/src/PoolDomain.ts +++ b/src/PoolDomain.ts @@ -1,4 +1,4 @@ -import { defer, of, switchMap, tap } from 'rxjs' +import { defer, map, switchMap, tap } from 'rxjs' import { getContract } from 'viem' import { ABI } from './abi/index.js' import type { Centrifuge } from './Centrifuge.js' @@ -10,14 +10,14 @@ import { repeatOnEvents } from './utils/rx.js' export class PoolDomain extends Entity { constructor( - centrifuge: Centrifuge, + _root: Centrifuge, public pool: Pool, public chainId: number ) { - super(centrifuge, ['pool', pool.id, 'domain', chainId]) + super(_root, ['pool', pool.id, 'domain', chainId]) } - manager() { + investManager() { return this._root._query(['domainManager', this.chainId], () => defer(async () => { const { router } = lpConfig[this.chainId]! @@ -25,7 +25,6 @@ export class PoolDomain extends Entity { const gatewayAddress = await getContract({ address: router, abi: ABI.Router, client }).read.gateway!() const managerAddress = await getContract({ address: gatewayAddress as any, abi: ABI.Gateway, client }).read .investmentManager!() - console.log('managerAddress', managerAddress) return managerAddress as HexString }) ) @@ -33,7 +32,7 @@ export class PoolDomain extends Entity { poolManager() { return this._root._query(['domainPoolManager', this.chainId], () => - this.manager().pipe( + this.investManager().pipe( switchMap((manager) => { return getContract({ address: manager, @@ -50,12 +49,14 @@ export class PoolDomain extends Entity { return this._query(['isActive'], () => this.poolManager().pipe( switchMap((manager) => { - return of( - getContract({ - address: manager, - abi: ABI.PoolManager, - client: this._root.getClient(this.chainId)!, - }).read.isPoolActive!([this.pool.id]) as Promise + return defer( + () => + this._root.getClient(this.chainId)!.readContract({ + address: manager, + abi: ABI.PoolManager, + functionName: 'isPoolActive', + args: [Number(this.pool.id)], + }) as Promise ).pipe( repeatOnEvents( this._root, @@ -77,38 +78,3 @@ export class PoolDomain extends Entity { ) } } - -// repeat({ -// delay: () => -// this._root.filteredEvents(manager, ABI.PoolManager, 'AddPool', this.chainId).pipe( -// filter((events) => { -// return events.some((event) => { -// return event.args.poolId === this.pool.id -// }) -// }) -// ), -// }) - -// repeatOnEvents( -// this._root, -// { -// address: manager, -// abi: ABI.PoolManager, -// eventName: 'AddPool', -// filter: (events) => { -// return events.some((event) => { -// return event.args.poolId === this.pool.id -// }) -// }, -// }, -// this.chainId -// ) - -// type ClassDecorator = ( -// value: Function, -// context: { -// kind: 'class'; -// name: string | undefined; -// addInitializer(initializer: () => void): void; -// } -// ) => Function | void; diff --git a/src/abi/Permit.abi.json b/src/abi/Permit.abi.json deleted file mode 100644 index f78c411..0000000 --- a/src/abi/Permit.abi.json +++ /dev/null @@ -1 +0,0 @@ -["function PERMIT_TYPEHASH() view returns (bytes32)"] diff --git a/src/abi/index.ts b/src/abi/index.ts index 4bb2f43..0b9f4fe 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -4,7 +4,6 @@ import Currency from './Currency.abi.js' import Gateway from './Gateway.abi.js' import InvestmentManager from './InvestmentManager.abi.js' import LiquidityPool from './LiquidityPool.abi.js' -import Permit from './Permit.abi.js' import PoolManager from './PoolManager.abi.js' import Router from './Router.abi.js' @@ -14,7 +13,6 @@ export const ABI = { Gateway: parseAbi(Gateway), InvestmentManager: parseAbi(InvestmentManager), LiquidityPool: parseAbi(LiquidityPool), - Permit: parseAbi(Permit), PoolManager: parseAbi(PoolManager), Router: parseAbi(Router), } diff --git a/src/config/lp.ts b/src/config/lp.ts index 1b38dd6..0b6b78e 100644 --- a/src/config/lp.ts +++ b/src/config/lp.ts @@ -1,6 +1,8 @@ +import type { HexString } from '../types/index.js' + type LPConfig = { - centrifugeRouter: `0x${string}` - router: `0x${string}` + centrifugeRouter: HexString + router: HexString } export const lpConfig: Record = { // Testnet diff --git a/src/constants.ts b/src/constants.ts index cdd3b68..86cdb19 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1,2 @@ export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' +export const PERMIT_TYPEHASH = '0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9' diff --git a/src/index.ts b/src/index.ts index 029c49e..67fce59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import { Centrifuge } from './Centrifuge.js' +export * from './Pool.js' +export * from './PoolDomain.js' export * from './types/index.js' export * from './types/query.js' export * from './types/transaction.js' From eb683b7d41b3b96b1d399d34941b79f2744c8ffa Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:07:06 +0100 Subject: [PATCH 28/53] config --- src/Centrifuge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index ac11283..e13c64a 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -48,7 +48,7 @@ export type UserProvidedConfig = Partial const envConfig = { mainnet: { - subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', + subqueryUrl: 'https://subql.embrio.tech/', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8ed99a9a115349bbbc01dcf3a24edc96', defaultChain: 1, From 17c6921aa3e7b3c78fcd7e16ef2c5050fbbcf7f7 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:08:18 +0100 Subject: [PATCH 29/53] feedback --- README.md | 2 +- src/Centrifuge.ts | 48 ++++++++++----------------- src/Pool.ts | 20 +++++------ src/{PoolDomain.ts => PoolNetwork.ts} | 13 ++++---- src/index.ts | 2 +- src/types/transaction.ts | 3 ++ src/utils/transaction.ts | 3 +- 7 files changed, 41 insertions(+), 50 deletions(-) rename src/{PoolDomain.ts => PoolNetwork.ts} (85%) diff --git a/README.md b/README.md index 913766a..4014fe2 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The following config options can be passed on initilization of CentrifugeSDK: - `rpcUrls: Record` - Optional - A object mapping chain ids to RPC URLs -- `subqueryUrl: string` +- `centrifugeApiUrl: string` - Optional ## Queries diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index e13c64a..d36aafe 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -48,19 +48,19 @@ export type UserProvidedConfig = Partial const envConfig = { mainnet: { - subqueryUrl: 'https://subql.embrio.tech/', + centrifugeApiUrl: 'https://subql.embrio.tech/', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8ed99a9a115349bbbc01dcf3a24edc96', defaultChain: 1, }, demo: { - subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', + centrifugeApiUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', defaultChain: 11155111, }, dev: { - subqueryUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', + centrifugeApiUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', defaultChain: 11155111, @@ -100,7 +100,7 @@ export class Centrifuge { const defaultConfigForEnv = envConfig[config?.environment ?? 'mainnet'] this.#config = { ...defaultConfig, - subqueryUrl: defaultConfigForEnv.subqueryUrl, + centrifugeApiUrl: defaultConfigForEnv.centrifugeApiUrl, defaultChain: defaultConfigForEnv.defaultChain, ...config, } @@ -180,8 +180,8 @@ export class Centrifuge { /** * @internal */ - _getSubqueryObservable(query: string, variables?: Record) { - return fromFetch(this.config.subqueryUrl, { + _getCentrifugeApiObservable(query: string, variables?: Record) { + return fromFetch(this.config.centrifugeApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -201,26 +201,30 @@ export class Centrifuge { /** * @internal */ - _querySubquery( + _queryCentrifugeApi( keys: (string | number)[] | null, query: string, variables?: Record ): Query - _querySubquery( + _queryCentrifugeApi( keys: (string | number)[] | null, query: string, variables: Record, postProcess: (data: Result) => Return ): Query - _querySubquery( + _queryCentrifugeApi( keys: (string | number)[] | null, query: string, variables?: Record, postProcess?: (data: Result) => Return ) { - return this._query(keys, () => this._getSubqueryObservable(query, variables).pipe(map(postProcess ?? identity)), { - valueCacheTime: 300, - }) + return this._query( + keys, + () => this._getCentrifugeApiObservable(query, variables).pipe(map(postProcess ?? identity)), + { + valueCacheTime: 300, + } + ) } #memoized = new Map() @@ -334,22 +338,6 @@ export class Centrifuge { return $shared } - /* - SCENARIO 1: - When the shared inner completes, the inner is reset - When a new subscriber subscribes after the shared inner completes, the shared inner is not re-created - Instead, it re-subscribes to the shared inner, which restarts - This causes existing subscribers to not receive the new values, because `createShared` is not called and thus `sharedSubject.next($shared)` is not called - - SCENARIO 2: - When the shared inner completes, the inner is not reset - In which case, it relies on the `valueCacheTime` to invalidate the cache which is infinite by default - - - What we want is a an Infinite valueCacheTime for infinite observables and a finite cache time for finite observables - - */ - const $query = createShared().pipe( // For new subscribers, recreate the shared observable if the previously shared observable has completed // and no longer has a cached value, which can happen with a finite `valueCacheTime`. @@ -403,7 +391,7 @@ export class Centrifuge { title: string, transactionCallback: (params: TransactionCallbackParams) => Promise | Observable, chainId?: number - ): Query { + ): Transaction { return this._transactSequence(async function* (params) { const transaction = transactionCallback(params) yield* doTransaction(title, params.publicClient, () => @@ -450,7 +438,7 @@ export class Centrifuge { params: TransactionCallbackParams ) => AsyncGenerator | Observable, chainId?: number - ): Query { + ): Transaction { const targetChainId = chainId ?? this.config.defaultChain const self = this async function* transact() { diff --git a/src/Pool.ts b/src/Pool.ts index 2fa28fa..97a6569 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -1,7 +1,7 @@ import { catchError, combineLatest, map, of, switchMap, timeout } from 'rxjs' import type { Centrifuge } from './Centrifuge.js' import { Entity } from './Entity.js' -import { PoolDomain } from './PoolDomain.js' +import { PoolNetwork } from './PoolNetwork.js' export class Pool extends Entity { constructor( @@ -11,18 +11,18 @@ export class Pool extends Entity { super(_root, ['pool', id]) } - domains() { + networks() { return this._query(null, () => { return of( this._root.chains.map((chainId) => { - return new PoolDomain(this._root, this, chainId) + return new PoolNetwork(this._root, this, chainId) }) ) }) } tranches() { - return this._root._querySubquery( + return this._root._queryCentrifugeApi( ['tranches', this.id], `query($poolId: String!) { pool(id: $poolId) { @@ -42,20 +42,20 @@ export class Pool extends Entity { ) } - activeDomains() { + activenetworks() { return this._query(null, () => { - return this.domains().pipe( - switchMap((domains) => { + return this.networks().pipe( + switchMap((networks) => { return combineLatest( - domains.map((domain) => - domain.isActive().pipe( + networks.map((network) => + network.isActive().pipe( timeout(8000), catchError(() => { return of(false) }) ) ) - ).pipe(map((isActive) => domains.filter((_, index) => isActive[index]))) + ).pipe(map((isActive) => networks.filter((_, index) => isActive[index]))) }) ) }) diff --git a/src/PoolDomain.ts b/src/PoolNetwork.ts similarity index 85% rename from src/PoolDomain.ts rename to src/PoolNetwork.ts index 3d496f6..41fd250 100644 --- a/src/PoolDomain.ts +++ b/src/PoolNetwork.ts @@ -1,4 +1,4 @@ -import { defer, map, switchMap, tap } from 'rxjs' +import { defer, switchMap } from 'rxjs' import { getContract } from 'viem' import { ABI } from './abi/index.js' import type { Centrifuge } from './Centrifuge.js' @@ -8,17 +8,17 @@ import type { Pool } from './Pool.js' import type { HexString } from './types/index.js' import { repeatOnEvents } from './utils/rx.js' -export class PoolDomain extends Entity { +export class PoolNetwork extends Entity { constructor( _root: Centrifuge, public pool: Pool, public chainId: number ) { - super(_root, ['pool', pool.id, 'domain', chainId]) + super(_root, ['pool', pool.id, 'network', chainId]) } investManager() { - return this._root._query(['domainManager', this.chainId], () => + return this._root._query(['investmentManager', this.chainId], () => defer(async () => { const { router } = lpConfig[this.chainId]! const client = this._root.getClient(this.chainId)! @@ -31,7 +31,7 @@ export class PoolDomain extends Entity { } poolManager() { - return this._root._query(['domainPoolManager', this.chainId], () => + return this._root._query(['poolManager', this.chainId], () => this.investManager().pipe( switchMap((manager) => { return getContract({ @@ -39,8 +39,7 @@ export class PoolDomain extends Entity { abi: ABI.InvestmentManager, client: this._root.getClient(this.chainId)!, }).read.poolManager!() as Promise - }), - tap((poolManager) => console.log('poolManager', poolManager)) + }) ) ) } diff --git a/src/index.ts b/src/index.ts index 67fce59..dff7167 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { Centrifuge } from './Centrifuge.js' export * from './Pool.js' -export * from './PoolDomain.js' +export * from './PoolNetwork.js' export * from './types/index.js' export * from './types/query.js' export * from './types/transaction.js' diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 2b8f433..a2b2452 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -1,5 +1,6 @@ import type { Account, Chain, LocalAccount, PublicClient, TransactionReceipt, WalletClient } from 'viem' import type { HexString } from './index.js' +import type { Query } from './query.js' export type OperationStatusType = | 'SwitchingChain' @@ -59,3 +60,5 @@ export type TransactionCallbackParams = { walletClient: WalletClient signer: Signer } + +export type Transaction = Query diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index c2f7c0e..f441257 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -42,7 +42,8 @@ export async function signPermit( amount: bigint ) { let domainOrCurrency: any = currencyAddress - if (currencyAddress.toLowerCase() === '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48') { + const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + if (currencyAddress.toLowerCase() === USDC) { // USDC has custom version domainOrCurrency = { name: 'USD Coin', version: '2', chainId, verifyingContract: currencyAddress } } else if (chainId === 5 || chainId === 84531 || chainId === 421613 || chainId === 11155111) { From 3a8275580d37ca7316110de8c195004bf0c81690 Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 12 Nov 2024 11:23:16 -0500 Subject: [PATCH 30/53] Impersonation for tests (overwrite from address in transactions) (#18) * Use signer in setup and remove unused conditional * Add impersonation functionality to tenderlyFork by overriding request * Add test file for accounts * Add minimal readme docs * Fix failing test * Add test case for sender address --- src/tests/Account.test.ts | 73 ++++++++++++++++++++++++++++++++++++ src/tests/Centrifuge.test.ts | 57 ++-------------------------- src/tests/README.md | 4 ++ src/tests/setup.ts | 6 +-- src/tests/tenderly.ts | 59 ++++++++++++++++++++++------- 5 files changed, 129 insertions(+), 70 deletions(-) create mode 100644 src/tests/Account.test.ts diff --git a/src/tests/Account.test.ts b/src/tests/Account.test.ts new file mode 100644 index 0000000..cf442bf --- /dev/null +++ b/src/tests/Account.test.ts @@ -0,0 +1,73 @@ +import { combineLatest, filter, last, firstValueFrom, Observable } from 'rxjs' +import { expect } from 'chai' +import { context } from './setup.js' +import { parseEther } from 'viem/utils' +import { type OperationConfirmedStatus } from '../types/transaction.js' + +describe('Account', () => { + it('should fetch account and balances', async function () { + const account = await context.centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f') + const balances = await account.balances() + expect(balances).to.exist + }) + + it('should make a transfer', async function () { + const fromAddress = this.context.tenderlyFork.account.address + const destAddress = '0x423420Ae467df6e90291fd0252c0A8a637C1e03f' + const transferAmount = 10_000_000n + + await Promise.all([ + context.tenderlyFork.fundAccountEth(fromAddress, parseEther('100')), + context.tenderlyFork.fundAccountERC20(fromAddress, 100_000_000n), + ]) + const fromAccount = await context.centrifuge.account(fromAddress) + const destAccount = await context.centrifuge.account(destAddress) + const fromBalanceInitial = await fromAccount.balances() + const destBalanceInitial = await destAccount.balances() + + const [transfer, fromBalanceFinal, destBalanceFinal] = await firstValueFrom( + combineLatest([ + fromAccount.transfer(destAddress, transferAmount).pipe(last()) as Observable, + fromAccount.balances().pipe(filter((balance) => balance !== fromBalanceInitial)), + destAccount.balances().pipe(filter((balance) => balance !== destBalanceInitial)), + ]) + ) + + expect(transfer.type).to.equal('TransactionConfirmed') + expect(transfer.title).to.equal('Transfer') + expect(transfer.receipt.status).to.equal('success') + expect(fromBalanceFinal).to.equal(fromBalanceInitial - transferAmount) + expect(destBalanceFinal).to.equal(destBalanceInitial + transferAmount) + }) + + it('should make a transfer impersonating the from address', async function () { + const impersonatedAddress = '0x423420Ae467df6e90291fd0252c0A8a637C1e03f' + context.tenderlyFork.impersonateAddress = impersonatedAddress + context.centrifuge.setSigner(context.tenderlyFork.signer) + const transferAmount = 1_000_000n + await Promise.all([ + context.tenderlyFork.fundAccountEth(impersonatedAddress, parseEther('100')), + context.tenderlyFork.fundAccountERC20(impersonatedAddress, transferAmount), + ]) + + const impersonatedAccount = await context.centrifuge.account(impersonatedAddress) + const destAddress = '0x26876eAceb62d31214C31F1a58b74A1445018b75' + const destAccount = await context.centrifuge.account(destAddress) + const destBalanceInitial = await destAccount.balances() + + const [transfer, impersonatedBalanceFinal, destBalanceFinal] = await firstValueFrom( + combineLatest([ + impersonatedAccount.transfer(destAddress, transferAmount).pipe(last()) as Observable, + impersonatedAccount.balances().pipe(filter((balance) => balance !== transferAmount)), + destAccount.balances().pipe(filter((balance) => balance !== destBalanceInitial)), + ]) + ) + + expect(transfer.receipt.from.toLowerCase()).to.equal(impersonatedAddress.toLowerCase()) + expect(transfer.type).to.equal('TransactionConfirmed') + expect(transfer.title).to.equal('Transfer') + expect(transfer.receipt.status).to.equal('success') + expect(impersonatedBalanceFinal).to.equal(0n) + expect(destBalanceFinal).to.equal(destBalanceInitial + transferAmount) + }) +}) diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 1a1000c..42eb5fb 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -1,24 +1,10 @@ import { expect } from 'chai' -import { - combineLatest, - defer, - filter, - firstValueFrom, - interval, - map, - Observable, - of, - Subject, - take, - tap, - toArray, -} from 'rxjs' +import { combineLatest, defer, firstValueFrom, interval, map, of, Subject, take, tap, toArray } from 'rxjs' import sinon from 'sinon' -import { createClient, custom, parseEther } from 'viem' -import { Centrifuge } from '../Centrifuge.js' -import type { OperationConfirmedStatus } from '../types/transaction.js' +import { createClient, custom } from 'viem' import { doSignMessage, doTransaction } from '../utils/transaction.js' import { context } from './setup.js' +import { Centrifuge } from '../Centrifuge.js' describe('Centrifuge', () => { let clock: sinon.SinonFakeTimers @@ -38,42 +24,7 @@ describe('Centrifuge', () => { expect(chains).to.include(11155111) }) - it('should fetch account and balances', async () => { - const account = await context.centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f') - const balances = await account.balances() - expect(balances).to.exist - }) - - it('should make a transfer', async function () { - const fromAddress = this.context.tenderlyFork.account.address - const destAddress = '0x423420Ae467df6e90291fd0252c0A8a637C1e03f' - const transferAmount = 10_000_000n - - await Promise.all([ - context.tenderlyFork.fundAccountEth(fromAddress, parseEther('100')), - context.tenderlyFork.fundAccountERC20(fromAddress, 100_000_000n), - ]) - const fromAccount = await context.centrifuge.account(fromAddress) - const destAccount = await context.centrifuge.account(destAddress) - const fromBalanceInitial = await fromAccount.balances() - const destBalanceInitial = await destAccount.balances() - - const [status, fromBalanceFinal, destBalanceFinal] = await firstValueFrom( - combineLatest([ - fromAccount.transfer(destAddress, transferAmount) as Observable, - fromAccount.balances().pipe(filter((balance) => balance !== fromBalanceInitial)), - destAccount.balances().pipe(filter((balance) => balance !== destBalanceInitial)), - ]) - ) - - expect(status.type).to.equal('TransactionConfirmed') - expect(status.title).to.equal('Transfer') - expect(status.receipt.status).to.equal('success') - expect(fromBalanceFinal).to.equal(fromBalanceInitial - transferAmount) - expect(destBalanceFinal).to.equal(destBalanceInitial + transferAmount) - }) - - it('should fetch a pool by id', async () => { + it('should fetch a pool by id', async function () { const pool = await context.centrifuge.pool('4139607887') expect(pool).to.exist }) diff --git a/src/tests/README.md b/src/tests/README.md index 4b1d20a..eb39c1f 100644 --- a/src/tests/README.md +++ b/src/tests/README.md @@ -49,4 +49,8 @@ await tenderlyFork.fundAccountEth(tenderlyFork.account.address, parseEther('100' // fund the signer's account with USDt ERC20 tokens await tenderlyFork.fundAccountERC20(tenderlyFork.account.address, parseEther('100')) + +// impersonate an address to send a transaction from that address +tenderlyFork.impersonateAddress = '0x...' +centrifuge.setSigner(tenderlyFork.signer) // setSigner must be called (again) after impersonateAddress is set ``` diff --git a/src/tests/setup.ts b/src/tests/setup.ts index ca2ee0f..fe7fbd8 100644 --- a/src/tests/setup.ts +++ b/src/tests/setup.ts @@ -5,7 +5,6 @@ import { TenderlyFork } from './tenderly.js' class TestContext { public centrifuge!: Centrifuge public tenderlyFork!: TenderlyFork - public allTestsSucceeded = true async initialize() { this.tenderlyFork = await TenderlyFork.create(sepolia) @@ -15,7 +14,7 @@ class TestContext { 11155111: this.tenderlyFork.rpcUrl, }, }) - this.centrifuge.setSigner(this.tenderlyFork.account) + this.centrifuge.setSigner(this.tenderlyFork.signer) } async cleanup() { @@ -23,10 +22,9 @@ class TestContext { console.log('DEBUG is true, RPC endpoint will not be deleted', this.tenderlyFork.rpcUrl) return } - if (this.tenderlyFork && this.allTestsSucceeded) { + if (this.tenderlyFork) { return this.tenderlyFork.deleteTenderlyRpcEndpoint() } - console.log('A test has failed, RPC endpoint will not be deleted', this.tenderlyFork.rpcUrl) } } diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts index d6d03a1..384318f 100644 --- a/src/tests/tenderly.ts +++ b/src/tests/tenderly.ts @@ -75,6 +75,10 @@ export class TenderlyFork { chain: Chain vnetId?: string _rpcUrl?: string + /** + * after impersonateAddress is set, centrifuge.setSigner() must be called again to update the signer + */ + impersonateAddress?: `0x${string}` forkedNetwork?: TenderlyVirtualNetwork get rpcUrl(): string { if (!this._rpcUrl) { @@ -92,17 +96,20 @@ export class TenderlyFork { } /** * if no account is set, one will be created randomly - * alternatively, this.account can set be set with `createAccount(privateKey)` + * if an impersonated address is set, a custom account will be created with that address which will override the fromAddress parameter in calls */ private _account?: LocalAccount get account(): LocalAccount { - return this._account ?? this.createAccount() + if (this.impersonateAddress) return this.createCustomAccount(this.impersonateAddress) + if (this._account) return this._account + return this.createAccount() } - constructor(chain: Chain, vnetId?: string, rpcUrl?: string) { + constructor(chain: Chain, vnetId?: string, rpcUrl?: string, impersonateAddress?: `0x${string}`) { this.chain = chain this.vnetId = vnetId this._rpcUrl = rpcUrl + this.impersonateAddress = impersonateAddress } public static async create(chain: Chain, vnetId?: string): Promise { @@ -128,24 +135,50 @@ export class TenderlyFork { } private setSigner(): WalletClient { - const walletAccount = this.account - this._signer = - this._signer ?? - createWalletClient({ - account: walletAccount, - transport: http(this.rpcUrl), - chain: this.chain, - }) - return this._signer! + if (this._signer) return this._signer + const client = createWalletClient({ + account: this.account, + transport: http(this.rpcUrl), + chain: this.chain, + rpcSchema: rpcSchema(), + }) + + const signer: ReturnType = { + ...client, + // Override the request method to use override from address with impersonated account + request: async (args) => { + if (args.method === 'eth_sendTransaction') { + // @ts-expect-error + const impersonatedParams = args.params.map((arg: any) => ({ ...arg, from: this.account.address })) + return client.request({ method: args.method, params: impersonatedParams }) + } + // @ts-expect-error + return client.request(args) + }, + } as WalletClient + this._signer = signer + return signer } - createAccount(privateKey?: Hex) { + createAccount(privateKey?: Hex): LocalAccount { const key = privateKey ?? generatePrivateKey() const walletAccount = privateKeyToAccount(key) this._account = walletAccount return walletAccount } + createCustomAccount(address: `0x${string}`): LocalAccount { + return { + address, + type: 'local', + source: 'custom', + signMessage: async ({ message }) => '0x' as `0x${string}`, + signTransaction: async (tx) => '0x' as `0x${string}`, + signTypedData: async (typedData) => '0x' as `0x${string}`, + publicKey: '0x' as `0x${string}`, + } + } + async forkNetwork() { try { const tenderlyApi = `${TENDERLY_API_URL}/account/${ACCOUNT_SLUG}/project/${PROJECT_SLUG}/vnets` From 5e6ec9e6a5989c2d4023a5bb85ae1531a8e3b29b Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:05:22 +0100 Subject: [PATCH 31/53] rename --- README.md | 2 +- src/Centrifuge.ts | 44 +++++++++++++++++++------------------------- src/Pool.ts | 2 +- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4014fe2..51d087a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The following config options can be passed on initilization of CentrifugeSDK: - `rpcUrls: Record` - Optional - A object mapping chain ids to RPC URLs -- `centrifugeApiUrl: string` +- `indexerUrl: string` - Optional ## Queries diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index d36aafe..786db1c 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -39,33 +39,36 @@ import { doTransaction, isLocalAccount } from './utils/transaction.js' export type Config = { environment: 'mainnet' | 'demo' | 'dev' rpcUrls?: Record - subqueryUrl: string } -type DerivedConfig = Config & { +export type UserProvidedConfig = Partial +type EnvConfig = { + indexerUrl: string + alchemyKey: string + infuraKey: string defaultChain: number } -export type UserProvidedConfig = Partial +type DerivedConfig = Config & EnvConfig const envConfig = { mainnet: { - centrifugeApiUrl: 'https://subql.embrio.tech/', + indexerUrl: 'https://subql.embrio.tech/', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8ed99a9a115349bbbc01dcf3a24edc96', defaultChain: 1, }, demo: { - centrifugeApiUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', + indexerUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', defaultChain: 11155111, }, dev: { - centrifugeApiUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', + indexerUrl: 'https://api.subquery.network/sq/centrifuge/pools-demo-multichain', alchemyKey: 'KNR-1LZhNqWOxZS2AN8AFeaiESBV10qZ', infuraKey: '8cd8e043ee8d4001b97a1c37e08fd9dd', defaultChain: 11155111, }, -} +} satisfies Record const defaultConfig = { environment: 'mainnet', @@ -100,8 +103,7 @@ export class Centrifuge { const defaultConfigForEnv = envConfig[config?.environment ?? 'mainnet'] this.#config = { ...defaultConfig, - centrifugeApiUrl: defaultConfigForEnv.centrifugeApiUrl, - defaultChain: defaultConfigForEnv.defaultChain, + ...defaultConfigForEnv, ...config, } Object.freeze(this.#config) @@ -180,8 +182,8 @@ export class Centrifuge { /** * @internal */ - _getCentrifugeApiObservable(query: string, variables?: Record) { - return fromFetch(this.config.centrifugeApiUrl, { + _getIndexerObservable(query: string, variables?: Record) { + return fromFetch(this.config.indexerUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -201,30 +203,22 @@ export class Centrifuge { /** * @internal */ - _queryCentrifugeApi( - keys: (string | number)[] | null, - query: string, - variables?: Record - ): Query - _queryCentrifugeApi( + _queryIndexer(keys: (string | number)[] | null, query: string, variables?: Record): Query + _queryIndexer( keys: (string | number)[] | null, query: string, variables: Record, postProcess: (data: Result) => Return ): Query - _queryCentrifugeApi( + _queryIndexer( keys: (string | number)[] | null, query: string, variables?: Record, postProcess?: (data: Result) => Return ) { - return this._query( - keys, - () => this._getCentrifugeApiObservable(query, variables).pipe(map(postProcess ?? identity)), - { - valueCacheTime: 300, - } - ) + return this._query(keys, () => this._getIndexerObservable(query, variables).pipe(map(postProcess ?? identity)), { + valueCacheTime: 120, + }) } #memoized = new Map() diff --git a/src/Pool.ts b/src/Pool.ts index 97a6569..772e828 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -22,7 +22,7 @@ export class Pool extends Entity { } tranches() { - return this._root._queryCentrifugeApi( + return this._root._queryIndexer( ['tranches', this.id], `query($poolId: String!) { pool(id: $poolId) { From 24d5ca3ab5cc717f868ae9669ef351988a1451e7 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:10:06 +0100 Subject: [PATCH 32/53] readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 51d087a..cf7acc2 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,6 @@ The following config options can be passed on initilization of CentrifugeSDK: - `rpcUrls: Record` - Optional - A object mapping chain ids to RPC URLs -- `indexerUrl: string` - - Optional ## Queries From 1627041c1d0d68aa08985eb81ccbfe6184c89745 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:16:19 +0100 Subject: [PATCH 33/53] cleanup --- src/Account.ts | 3 +-- src/PoolNetwork.ts | 50 ++++++++++++++++++++++++++++------------ src/abi/Gateway.abi.json | 2 +- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/Account.ts b/src/Account.ts index d43bf91..8c87ca6 100644 --- a/src/Account.ts +++ b/src/Account.ts @@ -35,7 +35,6 @@ export class Account extends Entity { abi: ABI.Currency, eventName: 'Transfer', filter: (events) => { - console.log('Transfer Event') return events.some((event) => { return event.args.from === this.accountId || event.args.to === this.accountId }) @@ -47,7 +46,7 @@ export class Account extends Entity { }) } - transfer(to: HexString, amount: bigint) { + transfer(to: string, amount: bigint) { return this._transact( 'Transfer', ({ walletClient }) => diff --git a/src/PoolNetwork.ts b/src/PoolNetwork.ts index 41fd250..1166577 100644 --- a/src/PoolNetwork.ts +++ b/src/PoolNetwork.ts @@ -17,28 +17,48 @@ export class PoolNetwork extends Entity { super(_root, ['pool', pool.id, 'network', chainId]) } - investManager() { - return this._root._query(['investmentManager', this.chainId], () => - defer(async () => { + gateway() { + return this._root._query(['gateway', this.chainId], () => + defer(() => { const { router } = lpConfig[this.chainId]! - const client = this._root.getClient(this.chainId)! - const gatewayAddress = await getContract({ address: router, abi: ABI.Router, client }).read.gateway!() - const managerAddress = await getContract({ address: gatewayAddress as any, abi: ABI.Gateway, client }).read - .investmentManager!() - return managerAddress as HexString + return this._root.getClient(this.chainId)!.readContract({ + address: router, + abi: ABI.Router, + functionName: 'gateway', + }) as Promise }) ) } + investmentManager() { + return this._root._query(['investmentManager', this.chainId], () => + this.gateway().pipe( + switchMap( + (gateway) => + this._root.getClient(this.chainId)!.readContract({ + address: gateway, + abi: ABI.Gateway, + functionName: 'investmentManager', + }) as Promise + ) + ) + ) + } + poolManager() { return this._root._query(['poolManager', this.chainId], () => - this.investManager().pipe( - switchMap((manager) => { - return getContract({ - address: manager, - abi: ABI.InvestmentManager, - client: this._root.getClient(this.chainId)!, - }).read.poolManager!() as Promise + this.gateway().pipe( + switchMap( + (gateway) => + this._root.getClient(this.chainId)!.readContract({ + address: gateway, + abi: ABI.Gateway, + functionName: 'poolManager', + }) as Promise + ) + ) + ) + } }) ) ) diff --git a/src/abi/Gateway.abi.json b/src/abi/Gateway.abi.json index 4dac24f..16a01f3 100644 --- a/src/abi/Gateway.abi.json +++ b/src/abi/Gateway.abi.json @@ -1 +1 @@ -["function investmentManager() view returns (address)"] +["function investmentManager() view returns (address)", "function poolManager() view returns (address)"] From 5a6f894d1b4c36416b4bcd3a23ec56724ea87ee5 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:23:28 +0100 Subject: [PATCH 34/53] comments --- src/Pool.ts | 30 ++++++++++++++++++------------ src/PoolNetwork.ts | 35 ++++++++++++++++++++++++++++------- src/tests/Centrifuge.test.ts | 4 ++-- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/Pool.ts b/src/Pool.ts index 772e828..7e97670 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -11,17 +11,7 @@ export class Pool extends Entity { super(_root, ['pool', id]) } - networks() { - return this._query(null, () => { - return of( - this._root.chains.map((chainId) => { - return new PoolNetwork(this._root, this, chainId) - }) - ) - }) - } - - tranches() { + trancheIds() { return this._root._queryIndexer( ['tranches', this.id], `query($poolId: String!) { @@ -42,7 +32,23 @@ export class Pool extends Entity { ) } - activenetworks() { + /** + * Get all networks where a pool can potentially be deployed. + */ + networks() { + return this._query(null, () => { + return of( + this._root.chains.map((chainId) => { + return new PoolNetwork(this._root, this, chainId) + }) + ) + }) + } + + /** + * Get the networks where a pool is active. It doesn't mean that any vaults are deployed there necessarily. + */ + activeNetworks() { return this._query(null, () => { return this.networks().pipe( switchMap((networks) => { diff --git a/src/PoolNetwork.ts b/src/PoolNetwork.ts index 1166577..5c2c09d 100644 --- a/src/PoolNetwork.ts +++ b/src/PoolNetwork.ts @@ -8,6 +8,9 @@ import type { Pool } from './Pool.js' import type { HexString } from './types/index.js' import { repeatOnEvents } from './utils/rx.js' +/** + * Query and interact with a pool on a specific network. + */ export class PoolNetwork extends Entity { constructor( _root: Centrifuge, @@ -17,7 +20,11 @@ export class PoolNetwork extends Entity { super(_root, ['pool', pool.id, 'network', chainId]) } - gateway() { + /** + * Get the routing contract that forwards incoming/outgoing messages. + * @internal + */ + _gateway() { return this._root._query(['gateway', this.chainId], () => defer(() => { const { router } = lpConfig[this.chainId]! @@ -30,9 +37,14 @@ export class PoolNetwork extends Entity { ) } - investmentManager() { + /** + * Get the main contract that vaults interact with for + * incoming and outgoing investment transactions. + * @internal + */ + _investmentManager() { return this._root._query(['investmentManager', this.chainId], () => - this.gateway().pipe( + this._gateway().pipe( switchMap( (gateway) => this._root.getClient(this.chainId)!.readContract({ @@ -45,9 +57,14 @@ export class PoolNetwork extends Entity { ) } - poolManager() { + /** + * Get the contract manages which pools & tranches exist, + * as well as managing allowed pool currencies, and incoming and outgoing transfers. + * @internal + */ + _poolManager() { return this._root._query(['poolManager', this.chainId], () => - this.gateway().pipe( + this._gateway().pipe( switchMap( (gateway) => this._root.getClient(this.chainId)!.readContract({ @@ -64,9 +81,13 @@ export class PoolNetwork extends Entity { ) } + /** + * Get whether the pool is active on this network. It's a prerequisite for deploying vaults, + * and doesn't indicate whether any vaults have been deployed. + */ isActive() { return this._query(['isActive'], () => - this.poolManager().pipe( + this._poolManager().pipe( switchMap((manager) => { return defer( () => @@ -74,7 +95,7 @@ export class PoolNetwork extends Entity { address: manager, abi: ABI.PoolManager, functionName: 'isPoolActive', - args: [Number(this.pool.id)], + args: [this.pool.id], }) as Promise ).pipe( repeatOnEvents( diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 42eb5fb..2ddba9a 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -2,9 +2,9 @@ import { expect } from 'chai' import { combineLatest, defer, firstValueFrom, interval, map, of, Subject, take, tap, toArray } from 'rxjs' import sinon from 'sinon' import { createClient, custom } from 'viem' +import { Centrifuge } from '../Centrifuge.js' import { doSignMessage, doTransaction } from '../utils/transaction.js' import { context } from './setup.js' -import { Centrifuge } from '../Centrifuge.js' describe('Centrifuge', () => { let clock: sinon.SinonFakeTimers @@ -24,7 +24,7 @@ describe('Centrifuge', () => { expect(chains).to.include(11155111) }) - it('should fetch a pool by id', async function () { + it('should fetch a pool by id', async () => { const pool = await context.centrifuge.pool('4139607887') expect(pool).to.exist }) From e40e6553ac50faf5d21011123bfb2ceea36ed597 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:08:03 +0100 Subject: [PATCH 35/53] fixes --- src/Centrifuge.ts | 97 +++++++++++++++++++++++++++++++-------- src/abi/Currency.abi.json | 8 +++- src/abi/index.ts | 14 +++--- src/tests/Account.test.ts | 8 ++-- src/tests/tenderly.ts | 34 ++++++++------ src/utils/query.ts | 38 +++++++++++++++ 6 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 src/utils/query.ts diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 786db1c..85c63a6 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -32,8 +32,9 @@ import { chains } from './config/chains.js' import { Pool } from './Pool.js' import type { HexString } from './types/index.js' import type { CentrifugeQueryOptions, Query } from './types/query.js' -import type { OperationStatus, Signer, TransactionCallbackParams } from './types/transaction.js' import { makeThenable, shareReplayWithDelayedReset } from './utils/rx.js' +import type { OperationStatus, Signer, Transaction, TransactionCallbackParams } from './types/transaction.js' +import { hashKey } from './utils/query.js' import { doTransaction, isLocalAccount } from './utils/transaction.js' export type Config = { @@ -129,9 +130,73 @@ export class Centrifuge { return this._query(null, () => of(new Account(this, address as any, chainId ?? this.config.defaultChain))) } + currency(address: string, chainId?: number): Query { + const curAddress = address.toLowerCase() + const cid = chainId ?? this.config.defaultChain + return this._query(['currency', curAddress, cid], () => + defer(async () => { + const contract = getContract({ + address: curAddress as any, + abi: ABI.Currency, + client: this.getClient(cid)!, + }) + const [decimals, name, symbol, supportsPermit] = await Promise.all([ + contract.read.decimals!() as Promise, + contract.read.name!() as Promise, + contract.read.symbol!() as Promise, + contract.read.PERMIT_TYPEHASH!() + .then((hash) => hash === PERMIT_TYPEHASH) + .catch(() => false), + ]) + return { + address: curAddress as any, + decimals, + name, + symbol, + chainId: cid, + supportsPermit, + } + }) + ) + } + + balance(currency: string, owner: string, chainId?: number) { + const address = owner.toLowerCase() + const cid = chainId ?? this.config.defaultChain + return this._query(['balance', currency, owner, cid], () => { + return this.currency(currency, cid).pipe( + switchMap(() => + defer( + () => + this.getClient(cid)!.readContract({ + address: currency as any, + abi: ABI.Currency, + functionName: 'balanceOf', + args: [address], + }) as Promise + ).pipe( + repeatOnEvents( + this, + { + address: currency, + abi: ABI.Currency, + eventName: 'Transfer', + filter: (events) => { + return events.some((event) => { + return event.args.from?.toLowerCase() === address || event.args.to?.toLowerCase() === address + }) + }, + }, + cid + ) + ) + ) + ) + }) + } + /** * Returns an observable of all events on a given chain. - * * @internal */ _events(chainId?: number) { @@ -158,7 +223,6 @@ export class Centrifuge { /** * Returns an observable of events on a given chain, filtered by name(s) and address(es). - * * @internal */ _filteredEvents(address: string | string[], abi: Abi | Abi[], eventName: string | string[], chainId?: number) { @@ -203,27 +267,29 @@ export class Centrifuge { /** * @internal */ - _queryIndexer(keys: (string | number)[] | null, query: string, variables?: Record): Query + _queryIndexer(query: string, variables?: Record): Query _queryIndexer( - keys: (string | number)[] | null, query: string, variables: Record, postProcess: (data: Result) => Return ): Query _queryIndexer( - keys: (string | number)[] | null, query: string, variables?: Record, postProcess?: (data: Result) => Return ) { - return this._query(keys, () => this._getIndexerObservable(query, variables).pipe(map(postProcess ?? identity)), { - valueCacheTime: 120, - }) + return this._query( + [query, variables], + () => this._getIndexerObservable(query, variables).pipe(map(postProcess ?? identity)), + { + valueCacheTime: 120, + } + ) } #memoized = new Map() - #memoizeWith(keys: (string | number)[], callback: () => T): T { - const cacheKey = JSON.stringify(keys) + #memoizeWith(keys: any[], callback: () => T): T { + const cacheKey = hashKey(keys) if (this.#memoized.has(cacheKey)) { return this.#memoized.get(cacheKey) } @@ -313,11 +379,7 @@ export class Centrifuge { * * @internal */ - _query( - keys: (string | number)[] | null, - observableCallback: () => Observable, - options?: CentrifugeQueryOptions - ): Query { + _query(keys: any[] | null, observableCallback: () => Observable, options?: CentrifugeQueryOptions): Query { function get() { const sharedSubject = new Subject>() function createShared(): Observable { @@ -352,7 +414,6 @@ export class Centrifuge { * Will additionally prompt the user to switch chains if they're not on the correct chain. * * @example - * * ```ts * const tx = this._transact( * 'Transfer', @@ -385,7 +446,7 @@ export class Centrifuge { title: string, transactionCallback: (params: TransactionCallbackParams) => Promise | Observable, chainId?: number - ): Transaction { + ) { return this._transactSequence(async function* (params) { const transaction = transactionCallback(params) yield* doTransaction(title, params.publicClient, () => diff --git a/src/abi/Currency.abi.json b/src/abi/Currency.abi.json index 81cacd9..2a0a03c 100644 --- a/src/abi/Currency.abi.json +++ b/src/abi/Currency.abi.json @@ -1,7 +1,13 @@ [ "event Approval(address indexed owner, address indexed spender, uint256 value)", "event Transfer(address indexed from, address indexed to, uint256 value)", + "function PERMIT_TYPEHASH() view returns (bytes32)", "function approve(address, uint) external returns (bool)", "function transfer(address, uint) external returns (bool)", - "function balanceOf(address) view returns (uint)" + "function balanceOf(address) view returns (uint)", + "function allowance(address, address) view returns (uint)", + "function decimals() view returns (uint8)", + "function name() view returns (string)", + "function symbol() view returns (string)", + "function checkTransferRestriction(address, address, uint) view returns (bool)" ] diff --git a/src/abi/index.ts b/src/abi/index.ts index 0b9f4fe..506cb4d 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -1,11 +1,11 @@ import { parseAbi } from 'viem' -import CentrifugeRouter from './CentrifugeRouter.abi.js' -import Currency from './Currency.abi.js' -import Gateway from './Gateway.abi.js' -import InvestmentManager from './InvestmentManager.abi.js' -import LiquidityPool from './LiquidityPool.abi.js' -import PoolManager from './PoolManager.abi.js' -import Router from './Router.abi.js' +import CentrifugeRouter from './CentrifugeRouter.abi.json' assert { type: 'json' } +import Currency from './Currency.abi.json' assert { type: 'json' } +import Gateway from './Gateway.abi.json' assert { type: 'json' } +import InvestmentManager from './InvestmentManager.abi.json' assert { type: 'json' } +import LiquidityPool from './LiquidityPool.abi.json' assert { type: 'json' } +import PoolManager from './PoolManager.abi.json' assert { type: 'json' } +import Router from './Router.abi.json' assert { type: 'json' } export const ABI = { CentrifugeRouter: parseAbi(CentrifugeRouter), diff --git a/src/tests/Account.test.ts b/src/tests/Account.test.ts index cf442bf..19b9f5c 100644 --- a/src/tests/Account.test.ts +++ b/src/tests/Account.test.ts @@ -1,11 +1,11 @@ -import { combineLatest, filter, last, firstValueFrom, Observable } from 'rxjs' import { expect } from 'chai' -import { context } from './setup.js' +import { combineLatest, filter, firstValueFrom, last, Observable } from 'rxjs' import { parseEther } from 'viem/utils' import { type OperationConfirmedStatus } from '../types/transaction.js' +import { context } from './setup.js' describe('Account', () => { - it('should fetch account and balances', async function () { + it('should fetch account and balances', async () => { const account = await context.centrifuge.account('0x423420Ae467df6e90291fd0252c0A8a637C1e03f') const balances = await account.balances() expect(balances).to.exist @@ -40,7 +40,7 @@ describe('Account', () => { expect(destBalanceFinal).to.equal(destBalanceInitial + transferAmount) }) - it('should make a transfer impersonating the from address', async function () { + it('should make a transfer impersonating the from address', async () => { const impersonatedAddress = '0x423420Ae467df6e90291fd0252c0A8a637C1e03f' context.tenderlyFork.impersonateAddress = impersonatedAddress context.centrifuge.setSigner(context.tenderlyFork.signer) diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts index 384318f..791186e 100644 --- a/src/tests/tenderly.ts +++ b/src/tests/tenderly.ts @@ -1,3 +1,4 @@ +import dotenv from 'dotenv' import { createPublicClient, createWalletClient, @@ -13,7 +14,6 @@ import { type WalletClient, } from 'viem' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' -import dotenv from 'dotenv' dotenv.config({ path: './src/tests/.env' }) type TenderlyVirtualNetwork = { @@ -78,7 +78,14 @@ export class TenderlyFork { /** * after impersonateAddress is set, centrifuge.setSigner() must be called again to update the signer */ - impersonateAddress?: `0x${string}` + private _impersonateAddress?: `0x${string}` + get impersonateAddress() { + return this._impersonateAddress + } + set impersonateAddress(address: `0x${string}` | undefined) { + this._impersonateAddress = address + this.setSigner() + } forkedNetwork?: TenderlyVirtualNetwork get rpcUrl(): string { if (!this._rpcUrl) { @@ -105,11 +112,10 @@ export class TenderlyFork { return this.createAccount() } - constructor(chain: Chain, vnetId?: string, rpcUrl?: string, impersonateAddress?: `0x${string}`) { + constructor(chain: Chain, vnetId?: string, rpcUrl?: string) { this.chain = chain this.vnetId = vnetId this._rpcUrl = rpcUrl - this.impersonateAddress = impersonateAddress } public static async create(chain: Chain, vnetId?: string): Promise { @@ -135,7 +141,6 @@ export class TenderlyFork { } private setSigner(): WalletClient { - if (this._signer) return this._signer const client = createWalletClient({ account: this.account, transport: http(this.rpcUrl), @@ -147,10 +152,13 @@ export class TenderlyFork { ...client, // Override the request method to use override from address with impersonated account request: async (args) => { - if (args.method === 'eth_sendTransaction') { - // @ts-expect-error - const impersonatedParams = args.params.map((arg: any) => ({ ...arg, from: this.account.address })) - return client.request({ method: args.method, params: impersonatedParams }) + switch (args.method) { + case 'eth_accounts': + return [this.account.address] + case 'eth_sendTransaction': + // @ts-expect-error + const impersonatedParams = args.params.map((arg: any) => ({ ...arg, from: this.account.address })) + return client.request({ method: args.method, params: impersonatedParams }) } // @ts-expect-error return client.request(args) @@ -172,10 +180,10 @@ export class TenderlyFork { address, type: 'local', source: 'custom', - signMessage: async ({ message }) => '0x' as `0x${string}`, - signTransaction: async (tx) => '0x' as `0x${string}`, - signTypedData: async (typedData) => '0x' as `0x${string}`, - publicKey: '0x' as `0x${string}`, + signMessage: async () => '0x', + signTransaction: async () => '0x', + signTypedData: async () => '0x', + publicKey: '0x', } } diff --git a/src/utils/query.ts b/src/utils/query.ts new file mode 100644 index 0000000..9a671e7 --- /dev/null +++ b/src/utils/query.ts @@ -0,0 +1,38 @@ +export function hashKey(key: any[]): string { + return JSON.stringify(key, (_, val) => + isPlainObject(val) + ? // Order the keys in the object alphabetically + Object.keys(val) + .sort() + .reduce((result, key) => { + result[key] = val[key] + return result + }, {} as any) + : val + ) +} + +function isObject(o: any) { + return Object.prototype.toString.call(o) === '[object Object]' +} + +// Copied from: https://github.com/jonschlinkert/is-plain-object +function isPlainObject(o: any) { + if (isObject(o) === false) return false + + // If has modified constructor + const ctor = o.constructor + if (ctor === undefined) return true + + // If has modified prototype + const prot = ctor.prototype + if (isObject(prot) === false) return false + + // If constructor does not have an Object-specific method + if (prot.hasOwnProperty('isPrototypeOf') === false) { + return false + } + + // Most likely a plain Object + return true +} From ce8abc630099cb8a9a60932579783daf935da30f Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:23:30 +0100 Subject: [PATCH 36/53] cleanup --- src/Centrifuge.ts | 67 +---------------------------------------------- 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 85c63a6..965720b 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -32,9 +32,9 @@ import { chains } from './config/chains.js' import { Pool } from './Pool.js' import type { HexString } from './types/index.js' import type { CentrifugeQueryOptions, Query } from './types/query.js' -import { makeThenable, shareReplayWithDelayedReset } from './utils/rx.js' import type { OperationStatus, Signer, Transaction, TransactionCallbackParams } from './types/transaction.js' import { hashKey } from './utils/query.js' +import { makeThenable, shareReplayWithDelayedReset } from './utils/rx.js' import { doTransaction, isLocalAccount } from './utils/transaction.js' export type Config = { @@ -130,71 +130,6 @@ export class Centrifuge { return this._query(null, () => of(new Account(this, address as any, chainId ?? this.config.defaultChain))) } - currency(address: string, chainId?: number): Query { - const curAddress = address.toLowerCase() - const cid = chainId ?? this.config.defaultChain - return this._query(['currency', curAddress, cid], () => - defer(async () => { - const contract = getContract({ - address: curAddress as any, - abi: ABI.Currency, - client: this.getClient(cid)!, - }) - const [decimals, name, symbol, supportsPermit] = await Promise.all([ - contract.read.decimals!() as Promise, - contract.read.name!() as Promise, - contract.read.symbol!() as Promise, - contract.read.PERMIT_TYPEHASH!() - .then((hash) => hash === PERMIT_TYPEHASH) - .catch(() => false), - ]) - return { - address: curAddress as any, - decimals, - name, - symbol, - chainId: cid, - supportsPermit, - } - }) - ) - } - - balance(currency: string, owner: string, chainId?: number) { - const address = owner.toLowerCase() - const cid = chainId ?? this.config.defaultChain - return this._query(['balance', currency, owner, cid], () => { - return this.currency(currency, cid).pipe( - switchMap(() => - defer( - () => - this.getClient(cid)!.readContract({ - address: currency as any, - abi: ABI.Currency, - functionName: 'balanceOf', - args: [address], - }) as Promise - ).pipe( - repeatOnEvents( - this, - { - address: currency, - abi: ABI.Currency, - eventName: 'Transfer', - filter: (events) => { - return events.some((event) => { - return event.args.from?.toLowerCase() === address || event.args.to?.toLowerCase() === address - }) - }, - }, - cid - ) - ) - ) - ) - }) - } - /** * Returns an observable of all events on a given chain. * @internal From deff72374ebe3d42116fad985b341411f39e0028 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:00:40 +0100 Subject: [PATCH 37/53] brackets --- src/PoolNetwork.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PoolNetwork.ts b/src/PoolNetwork.ts index 5c2c09d..8108cdf 100644 --- a/src/PoolNetwork.ts +++ b/src/PoolNetwork.ts @@ -76,10 +76,6 @@ export class PoolNetwork extends Entity { ) ) } - }) - ) - ) - } /** * Get whether the pool is active on this network. It's a prerequisite for deploying vaults, From 4977f9e4ed61f3c97e4bb804b607a1dff050f42b Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:45:38 +0100 Subject: [PATCH 38/53] fix keys --- src/Pool.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Pool.ts b/src/Pool.ts index 7e97670..05d04c6 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -13,7 +13,6 @@ export class Pool extends Entity { trancheIds() { return this._root._queryIndexer( - ['tranches', this.id], `query($poolId: String!) { pool(id: $poolId) { tranches { From 55d73cf902a220f40c8fe2d63755118bb1c3231d Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:51:04 +0100 Subject: [PATCH 39/53] add vault entity --- src/Centrifuge.ts | 72 ++++- src/PoolNetwork.ts | 81 +++++- src/Vault.ts | 414 +++++++++++++++++++++++++++++ src/abi/InvestmentManager.abi.json | 5 +- src/abi/LiquidityPool.abi.json | 90 ++++++- src/config/lp.ts | 16 ++ src/tests/Vault.test.ts | 55 ++++ 7 files changed, 717 insertions(+), 16 deletions(-) create mode 100644 src/Vault.ts create mode 100644 src/tests/Vault.test.ts diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 965720b..d3d890a 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -11,6 +11,7 @@ import { mergeMap, of, Subject, + switchMap, using, } from 'rxjs' import { fromFetch } from 'rxjs/fetch' @@ -18,6 +19,7 @@ import { createPublicClient, createWalletClient, custom, + getContract, http, parseEventLogs, type Abi, @@ -27,14 +29,17 @@ import { type WalletClient, type WatchEventOnLogsParameter, } from 'viem' +import { ABI } from './abi/index.js' import { Account } from './Account.js' import { chains } from './config/chains.js' +import type { CurrencyMetadata } from './config/lp.js' +import { PERMIT_TYPEHASH } from './constants.js' import { Pool } from './Pool.js' import type { HexString } from './types/index.js' import type { CentrifugeQueryOptions, Query } from './types/query.js' import type { OperationStatus, Signer, Transaction, TransactionCallbackParams } from './types/transaction.js' import { hashKey } from './utils/query.js' -import { makeThenable, shareReplayWithDelayedReset } from './utils/rx.js' +import { makeThenable, repeatOnEvents, shareReplayWithDelayedReset } from './utils/rx.js' import { doTransaction, isLocalAccount } from './utils/transaction.js' export type Config = { @@ -130,6 +135,71 @@ export class Centrifuge { return this._query(null, () => of(new Account(this, address as any, chainId ?? this.config.defaultChain))) } + currency(address: string, chainId?: number): Query { + const curAddress = address.toLowerCase() + const cid = chainId ?? this.config.defaultChain + return this._query(['currency', curAddress, cid], () => + defer(async () => { + const contract = getContract({ + address: curAddress as any, + abi: ABI.Currency, + client: this.getClient(cid)!, + }) + const [decimals, name, symbol, supportsPermit] = await Promise.all([ + contract.read.decimals!() as Promise, + contract.read.name!() as Promise, + contract.read.symbol!() as Promise, + contract.read.PERMIT_TYPEHASH!() + .then((hash) => hash === PERMIT_TYPEHASH) + .catch(() => false), + ]) + return { + address: curAddress as any, + decimals, + name, + symbol, + chainId: cid, + supportsPermit, + } + }) + ) + } + + balance(currency: string, owner: string, chainId?: number) { + const address = owner.toLowerCase() + const cid = chainId ?? this.config.defaultChain + return this._query(['balance', currency, owner, cid], () => { + return this.currency(currency, cid).pipe( + switchMap(() => + defer( + () => + this.getClient(cid)!.readContract({ + address: currency as any, + abi: ABI.Currency, + functionName: 'balanceOf', + args: [address], + }) as Promise + ).pipe( + repeatOnEvents( + this, + { + address: currency, + abi: ABI.Currency, + eventName: 'Transfer', + filter: (events) => { + return events.some((event) => { + return event.args.from?.toLowerCase() === address || event.args.to?.toLowerCase() === address + }) + }, + }, + cid + ) + ) + ) + ) + }) + } + /** * Returns an observable of all events on a given chain. * @internal diff --git a/src/PoolNetwork.ts b/src/PoolNetwork.ts index 8108cdf..975a31a 100644 --- a/src/PoolNetwork.ts +++ b/src/PoolNetwork.ts @@ -1,4 +1,4 @@ -import { defer, switchMap } from 'rxjs' +import { combineLatest, defer, map, switchMap } from 'rxjs' import { getContract } from 'viem' import { ABI } from './abi/index.js' import type { Centrifuge } from './Centrifuge.js' @@ -7,6 +7,7 @@ import { Entity } from './Entity.js' import type { Pool } from './Pool.js' import type { HexString } from './types/index.js' import { repeatOnEvents } from './utils/rx.js' +import { Vault } from './Vault.js' /** * Query and interact with a pool on a specific network. @@ -77,6 +78,84 @@ export class PoolNetwork extends Entity { ) } + /** + * Get the deployed Vaults for a given tranche. There may exist one Vault for each allowed investment currency. + * Vaults are used to submit/claim investments and redemptions. + * @param trancheId - The tranche ID + */ + vaults(trancheId: string) { + return this._query(['vaults', trancheId], () => + this._poolManager().pipe( + switchMap((poolManager) => + defer(async () => { + const { currencies } = lpConfig[this.chainId]! + if (!currencies.length) return [] + const contract = getContract({ + address: poolManager, + abi: ABI.PoolManager, + client: this._root.getClient(this.chainId)!, + }) + const results = await Promise.allSettled( + currencies.map(async (curAddr) => { + const vaultAddr = (await contract.read.getVault!([this.pool.id, trancheId, curAddr])) as HexString + return new Vault(this._root, this, trancheId, curAddr, vaultAddr) + }) + ) + console.log('results', results) + return results.filter((result) => result.status === 'fulfilled').map((result) => result.value) + }).pipe( + repeatOnEvents( + this._root, + { + address: poolManager, + abi: ABI.PoolManager, + eventName: 'DeployVault', + filter: (events) => { + return events.some((event) => { + return String(event.args.poolId) === this.pool.id || event.args.trancheId === trancheId + }) + }, + }, + this.chainId + ) + ) + ) + ) + ) + } + + /** + * Get all Vaults for all tranches in the pool. + */ + vaultsByTranche() { + return this._query(null, () => + this.pool.trancheIds().pipe( + switchMap((tranches) => { + return combineLatest(tranches.map((trancheId) => this.vaults(trancheId))).pipe( + map((vaults) => Object.fromEntries(vaults.flat().map((vault, index) => [tranches[index], vault]))) + ) + }) + ) + ) + } + + /** + * Get a specific Vault for a given tranche and investment currency. + * @param trancheId - The tranche ID + * @param asset - The investment currency address + */ + vault(trancheId: string, asset: string) { + return this._query(null, () => + this.vaults(trancheId).pipe( + map((vaults) => { + const vault = vaults.find((v) => v._asset === asset) + if (!vault) throw new Error('Vault not found') + return vault + }) + ) + ) + } + /** * Get whether the pool is active on this network. It's a prerequisite for deploying vaults, * and doesn't indicate whether any vaults have been deployed. diff --git a/src/Vault.ts b/src/Vault.ts new file mode 100644 index 0000000..f8b31cd --- /dev/null +++ b/src/Vault.ts @@ -0,0 +1,414 @@ +import { combineLatest, defer, map, switchMap } from 'rxjs' +import { encodeFunctionData, getContract, toHex } from 'viem' +import type { Centrifuge } from './Centrifuge.js' +import { Entity } from './Entity.js' +import type { Pool } from './Pool.js' +import { PoolNetwork } from './PoolNetwork.js' +import { ABI } from './abi/index.js' +import { lpConfig } from './config/lp.js' +import type { HexString } from './types/index.js' +import { repeatOnEvents } from './utils/rx.js' +import { doSignMessage, doTransaction, signPermit, type Permit } from './utils/transaction.js' + +/** + * Query and interact with a vault, which is the main entry point for investing and redeeming funds. + * A vault is the combination of a network, a pool, a tranche and an investment currency. + */ +export class Vault extends Entity { + pool: Pool + chainId: number + /** + * The contract address of the investment currency. + * @internal + */ + _asset: HexString + /** + * The contract address of the vault. + */ + address: HexString + constructor( + _root: Centrifuge, + public network: PoolNetwork, + public trancheId: string, + asset: HexString, + address: HexString + ) { + super(_root, ['vault', network.chainId, network.pool.id, trancheId, asset.toLowerCase()]) + this.chainId = network.chainId + this.pool = network.pool + this._asset = asset.toLowerCase() as HexString + this.address = address.toLowerCase() as HexString + } + + /** + * Estimates the gas cost needed to bridge the message that results from a transaction. + * @internal + */ + _estimate() { + return this._root._query(['estimate'], () => + defer(() => { + const bytes = toHex(new Uint8Array([0x12])) + const { centrifugeRouter } = lpConfig[this.chainId]! + return this._root.getClient(this.chainId)!.readContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName: 'estimate', + args: [bytes], + }) as Promise + }) + ) + } + + /** + * Get the contract address of the share token. + * @internal + */ + _share() { + return this._query(['share'], () => + defer( + () => + this._root.getClient(this.chainId)!.readContract({ + address: this.address, + abi: ABI.LiquidityPool, + functionName: 'share', + }) as Promise + ) + ) + } + + /** + * Get the details of the investment currency. + */ + investmentCurrency() { + return this._query(null, () => this._root.currency(this._asset, this.chainId)) + } + + /** + * Get the details of the share token. + */ + shareCurrency() { + return this._query(null, () => this._share().pipe(switchMap((share) => this._root.currency(share, this.chainId)))) + } + + /** + * Get the allowance of the investment currency for the CentrifugeRouter, + * which is the contract that moves funds into the vault on behalf of the investor. + * @param owner - The address of the owner + */ + allowance(owner: string) { + const address = owner.toLowerCase() + return this._query(['allowance', address], () => + defer( + () => + this._root.getClient(this.chainId)!.readContract({ + address: this._asset, + abi: ABI.Currency, + functionName: 'allowance', + args: [address, lpConfig[this.chainId]!.centrifugeRouter], + }) as Promise + ).pipe( + repeatOnEvents( + this._root, + { + address: this._asset, + abi: ABI.Currency, + eventName: ['Approval', 'Transfer'], + filter: (events) => { + return events.some((event) => { + return ( + event.args.owner?.toLowerCase() === address || + event.args.spender?.toLowerCase() === this._asset || + event.args.from?.toLowerCase() === address + ) + }) + }, + }, + this.chainId + ) + ) + ) + } + + /** + * Get the details of the investment of an investor in the vault and any pending investments or redemptions. + * @param investor - The address of the investor + */ + investment(investor: string) { + const address = investor.toLowerCase() as HexString + return this._query(['investment', address], () => + combineLatest([this.investmentCurrency(), this.shareCurrency(), this.network._investmentManager()]).pipe( + switchMap(([investmentCurrency, shareCurrency, investmentManagerAddress]) => + combineLatest([ + this._root.balance(investmentCurrency.address, address, this.chainId), + this._root.balance(shareCurrency.address, address, this.chainId), + this.allowance(address), + defer(async () => { + const client = this._root.getClient(this.chainId)! + const vault = getContract({ address: this.address, abi: ABI.LiquidityPool, client }) + const investmentManager = getContract({ + address: investmentManagerAddress, + abi: ABI.InvestmentManager, + client, + }) + + const [isAllowedToInvest, maxDeposit, maxRedeem, investment] = await Promise.all([ + vault.read.isPermissioned!([address]) as Promise, + vault.read.maxDeposit!([address]) as Promise, + vault.read.maxRedeem!([address]) as Promise, + investmentManager.read.investments!([this.address, address]) as Promise< + [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint, boolean, boolean] + >, + ]) + + const [ + maxMint, + maxWithdraw, + , + , + pendingInvest, + pendingRedeem, + claimableCancelDepositCurrency, + claimableCancelRedeemShares, + hasPendingCancelDepositRequest, + hasPendingCancelRedeemRequest, + ] = investment + console.log('pendingInvest', pendingInvest) + return { + isAllowedToInvest, + claimableInvestShares: maxMint, + claimableInvestCurrencyEquivalent: maxDeposit, + claimableRedeemCurrency: maxWithdraw, + claimableRedeemSharesEquivalent: maxRedeem, + pendingInvestCurrency: pendingInvest, + pendingRedeemShares: pendingRedeem, + claimableCancelDepositCurrency, + claimableCancelRedeemShares, + hasPendingCancelDepositRequest, + hasPendingCancelRedeemRequest, + investmentCurrency, + shareCurrency, + } + }).pipe( + repeatOnEvents( + this._root, + { + address: this.address, + abi: ABI.LiquidityPool, + eventName: [ + 'CancelDepositClaim', + 'CancelDepositClaimable', + 'CancelDepositRequest', + 'CancelRedeemClaim', + 'CancelRedeemClaimable', + 'CancelRedeemRequest', + 'Deposit', + 'DepositClaimable', + 'DepositRequest', + 'RedeemClaimable', + 'RedeemRequest', + 'Withdraw', + ], + filter: (events) => { + console.log('events', events) + return events.some( + (event) => + event.args.receiver === investor || + event.args.controller === investor || + event.args.sender === investor || + event.args.owner === investor + ) + }, + }, + this.chainId + ) + ), + ]) + ), + map(([currencyBalance, shareBalance, allowance, investment]) => ({ + ...investment, + shareBalance, + investmentCurrencyBalance: currencyBalance, + investmentCurrencyAllowance: allowance, + })) + ) + ) + } + + /** + * Place an order to invest funds in the vault. If the amount is 0, it will request to cancel an open order. + * @param investAmount - The amount to invest in the vault + */ + placeInvestOrder(investAmount: bigint) { + const self = this + return this._transactSequence(async function* ({ walletClient, publicClient, signer, signingAddress }) { + const { centrifugeRouter } = lpConfig[self.chainId]! + const [estimate, investment] = await Promise.all([self._estimate(), self.investment(signingAddress)]) + const { + investmentCurrency, + investmentCurrencyBalance, + investmentCurrencyAllowance, + isAllowedToInvest, + pendingInvestCurrency, + } = investment + const supportsPermit = investmentCurrency.supportsPermit && 'send' in signer + const needsApproval = investmentCurrencyAllowance < investAmount + + if (!isAllowedToInvest) throw new Error('Not allowed to invest') + if (investAmount === 0n && pendingInvestCurrency === 0n) throw new Error('No order to cancel') + if (investAmount > investmentCurrencyBalance) throw new Error('Insufficient balance') + if (pendingInvestCurrency > 0n) throw new Error('Cannot change order') + + if (investAmount === 0n) { + yield* doTransaction('Cancel Invest Order', publicClient, () => + walletClient.writeContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName: 'cancelDepositRequest', + args: [self.address, estimate], + value: estimate, + }) + ) + return + } + + let permit: Permit | null = null + if (needsApproval) { + if (supportsPermit) { + permit = yield* doSignMessage('Sign Permit', () => + signPermit( + walletClient, + signer, + self.chainId, + signingAddress, + investmentCurrency.address, + centrifugeRouter, + investAmount + ) + ) + } else { + yield* doTransaction('Approve', publicClient, () => + walletClient.writeContract({ + address: investmentCurrency.address, + abi: ABI.Currency, + functionName: 'approve', + args: [centrifugeRouter, investAmount], + }) + ) + } + } + + const enableData = encodeFunctionData({ + abi: ABI.CentrifugeRouter, + functionName: 'enableLockDepositRequest', + args: [self.address, investAmount], + }) + const requestData = encodeFunctionData({ + abi: ABI.CentrifugeRouter, + functionName: 'executeLockedDepositRequest', + args: [self.address, signingAddress, estimate], + }) + const permitData = + permit && + encodeFunctionData({ + abi: ABI.CentrifugeRouter, + functionName: 'permit', + args: [ + investmentCurrency.address, + centrifugeRouter, + investAmount.toString(), + permit.deadline, + permit.v, + permit.r, + permit.s, + ], + }) + yield* doTransaction('Invest', publicClient, () => + walletClient.writeContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName: 'multicall', + args: [[enableData, requestData, permitData].filter(Boolean)], + value: estimate, + }) + ) + }, this.chainId) + } + + /** + * Cancel an open investment order. + */ + cancelInvestOrder() { + return this.placeInvestOrder(0n) + } + + /** + * Place an order to redeem funds from the vault. If the amount is 0, it will request to cancel an open order. + * @param shares - The amount of shares to redeem + */ + placeRedeemOrder(shares: bigint) { + const self = this + return this._transactSequence(async function* ({ walletClient, publicClient, signingAddress }) { + const { centrifugeRouter } = lpConfig[self.chainId]! + const [estimate, investment] = await Promise.all([self._estimate(), self.investment(signingAddress)]) + const { shareBalance, pendingRedeemShares } = investment + + if (shares === 0n && pendingRedeemShares === 0n) throw new Error('No order to cancel') + if (shares > shareBalance) throw new Error('Insufficient balance') + if (shares === 0n) { + yield* doTransaction('Cancel Redeem Order', publicClient, () => + walletClient.writeContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName: 'cancelRedeemRequest', + args: [self.address, estimate], + value: estimate, + }) + ) + return + } + yield* doTransaction('Redeem', publicClient, () => + walletClient.writeContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName: 'redeem', + args: [self.address, shares, signingAddress, signingAddress, estimate], + value: estimate, + }) + ) + }, this.chainId) + } + + /** + * Cancel an open redemption order. + */ + cancelRedeemOrder() { + return this.placeRedeemOrder(0n) + } + + /** + * Claim any outstanding fund shares after an investment has gone through, or funds after an redemption has gone through. + * @param receiver - The address that should receive the funds. If not provided, the investor's address is used. + */ + claim(receiver?: string) { + return this._transact('Claim', async ({ walletClient, signingAddress }) => { + const { centrifugeRouter } = lpConfig[this.chainId]! + const investment = await this.investment(signingAddress) + const receiverAddress = receiver || signingAddress + const functionName = + investment.claimableCancelDepositCurrency > 0n + ? 'claimCancelDepositRequest' + : investment.claimableCancelRedeemShares > 0n + ? 'claimCancelRedeemRequest' + : investment.claimableInvestShares > 0n + ? 'claimDeposit' + : 'claimRedeem' + + return walletClient.writeContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName, + args: [this.address, receiverAddress, signingAddress], + }) + }) + } +} diff --git a/src/abi/InvestmentManager.abi.json b/src/abi/InvestmentManager.abi.json index 06adf46..c3a16b1 100644 --- a/src/abi/InvestmentManager.abi.json +++ b/src/abi/InvestmentManager.abi.json @@ -1 +1,4 @@ -["function poolManager() view returns (address)"] +[ + "function poolManager() view returns (address)", + "function investments(address, address) view returns (uint128, uint128, uint256, uint256, uint128, uint128, uint128, uint128, bool, bool)" +] diff --git a/src/abi/LiquidityPool.abi.json b/src/abi/LiquidityPool.abi.json index ab4b837..c7d42f1 100644 --- a/src/abi/LiquidityPool.abi.json +++ b/src/abi/LiquidityPool.abi.json @@ -1,15 +1,79 @@ [ - "event DepositRequest(address indexed, address indexed, uint256 indexed, address, uint256)", - "event RedeemRequest(address indexed, address indexed, uint256 indexed, address, uint256)", - "event CancelDepositRequest(address indexed)", - "event CancelRedeemRequest(address indexed)", - "function mint(uint256, address) public returns (uint256)", - "function withdraw(uint256, address, address) public returns (uint256)", - "function requestDeposit(uint256, address, address) public", - "function requestRedeem(uint256, address, address) public", - "function requestDepositWithPermit(uint256, address, bytes, uint256, uint8, bytes32, bytes32) public", - "function cancelDepositRequest(uint256, address) public", - "function cancelRedeemRequest(uint256, address) public", - "function claimCancelDepositRequest(uint256, address, address) public", - "function claimCancelRedeemRequest(uint256, address, address) public" + "event CancelDepositClaim(address indexed receiver, address indexed controller, uint256 indexed requestId, address sender, uint256 assets)", + "event CancelDepositClaimable(address indexed controller, uint256 indexed requestId, uint256 assets)", + "event CancelDepositRequest(address indexed controller, uint256 indexed requestId, address sender)", + "event CancelRedeemClaim(address indexed receiver, address indexed controller, uint256 indexed requestId, address sender, uint256 shares)", + "event CancelRedeemClaimable(address indexed controller, uint256 indexed requestId, uint256 shares)", + "event CancelRedeemRequest(address indexed controller, uint256 indexed requestId, address sender)", + "event Deny(address indexed user)", + "event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares)", + "event DepositClaimable(address indexed controller, uint256 indexed requestId, uint256 assets, uint256 shares)", + "event DepositRequest(address indexed controller, address indexed owner, uint256 indexed requestId, address sender, uint256 assets)", + "event File(bytes32 indexed what, address data)", + "event OperatorSet(address indexed controller, address indexed operator, bool approved)", + "event RedeemClaimable(address indexed controller, uint256 indexed requestId, uint256 assets, uint256 shares)", + "event RedeemRequest(address indexed controller, address indexed owner, uint256 indexed requestId, address sender, uint256 assets)", + "event Rely(address indexed user)", + "event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)", + "function AUTHORIZE_OPERATOR_TYPEHASH() view returns (bytes32)", + "function DOMAIN_SEPARATOR() view returns (bytes32)", + "function asset() view returns (address)", + "function authorizations(address controller, bytes32 nonce) view returns (bool used)", + "function authorizeOperator(address controller, address operator, bool approved, bytes32 nonce, uint256 deadline, bytes signature) returns (bool success)", + "function cancelDepositRequest(uint256, address controller)", + "function cancelRedeemRequest(uint256, address controller)", + "function claimCancelDepositRequest(uint256, address receiver, address controller) returns (uint256 assets)", + "function claimCancelRedeemRequest(uint256, address receiver, address controller) returns (uint256 shares)", + "function claimableCancelDepositRequest(uint256, address controller) view returns (uint256 claimableAssets)", + "function claimableCancelRedeemRequest(uint256, address controller) view returns (uint256 claimableShares)", + "function claimableDepositRequest(uint256, address controller) view returns (uint256 claimableAssets)", + "function claimableRedeemRequest(uint256, address controller) view returns (uint256 claimableShares)", + "function convertToAssets(uint256 shares) view returns (uint256 assets)", + "function convertToShares(uint256 assets) view returns (uint256 shares)", + "function deny(address user)", + "function deploymentChainId() view returns (uint256)", + "function deposit(uint256 assets, address receiver, address controller) returns (uint256 shares)", + "function deposit(uint256 assets, address receiver) returns (uint256 shares)", + "function escrow() view returns (address)", + "function file(bytes32 what, address data)", + "function invalidateNonce(bytes32 nonce)", + "function isOperator(address, address) view returns (bool)", + "function isPermissioned(address controller) view returns (bool)", + "function manager() view returns (address)", + "function maxDeposit(address controller) view returns (uint256 maxAssets)", + "function maxMint(address controller) view returns (uint256 maxShares)", + "function maxRedeem(address controller) view returns (uint256 maxShares)", + "function maxWithdraw(address controller) view returns (uint256 maxAssets)", + "function mint(uint256 shares, address receiver) returns (uint256 assets)", + "function mint(uint256 shares, address receiver, address controller) returns (uint256 assets)", + "function onCancelDepositClaimable(address controller, uint256 assets)", + "function onCancelRedeemClaimable(address controller, uint256 shares)", + "function onDepositClaimable(address controller, uint256 assets, uint256 shares)", + "function onRedeemClaimable(address controller, uint256 assets, uint256 shares)", + "function onRedeemRequest(address controller, address owner, uint256 shares)", + "function pendingCancelDepositRequest(uint256, address controller) view returns (bool isPending)", + "function pendingCancelRedeemRequest(uint256, address controller) view returns (bool isPending)", + "function pendingDepositRequest(uint256, address controller) view returns (uint256 pendingAssets)", + "function pendingRedeemRequest(uint256, address controller) view returns (uint256 pendingShares)", + "function poolId() view returns (uint64)", + "function previewDeposit(uint256) pure returns (uint256)", + "function previewMint(uint256) pure returns (uint256)", + "function previewRedeem(uint256) pure returns (uint256)", + "function previewWithdraw(uint256) pure returns (uint256)", + "function priceLastUpdated() view returns (uint64)", + "function pricePerShare() view returns (uint256)", + "function recoverTokens(address token, address to, uint256 amount)", + "function redeem(uint256 shares, address receiver, address controller) returns (uint256 assets)", + "function rely(address user)", + "function requestDeposit(uint256 assets, address controller, address owner) returns (uint256)", + "function requestRedeem(uint256 shares, address controller, address owner) returns (uint256)", + "function root() view returns (address)", + "function setEndorsedOperator(address owner, bool approved)", + "function setOperator(address operator, bool approved) returns (bool success)", + "function share() view returns (address)", + "function supportsInterface(bytes4 interfaceId) pure returns (bool)", + "function totalAssets() view returns (uint256)", + "function trancheId() view returns (bytes16)", + "function wards(address) view returns (uint256)", + "function withdraw(uint256 assets, address receiver, address controller) returns (uint256 shares)" ] diff --git a/src/config/lp.ts b/src/config/lp.ts index 0b6b78e..5a0c33c 100644 --- a/src/config/lp.ts +++ b/src/config/lp.ts @@ -3,32 +3,48 @@ import type { HexString } from '../types/index.js' type LPConfig = { centrifugeRouter: HexString router: HexString + currencies: HexString[] } export const lpConfig: Record = { // Testnet 11155111: { centrifugeRouter: '0x723635430aa191ef5f6f856415f41b1a4d81dd7a', router: '0x130ce3f3c17b4458d6d4dfdf58a86aa2d261662e', + currencies: ['0x8503b4452Bf6238cC76CdbEE223b46d7196b1c93', '0xe2ac3c946445f9ff45ddce8acf17c93b7dd6295a'], }, 84532: { centrifugeRouter: '0x723635430aa191ef5f6f856415f41b1a4d81dd7a', router: '0xec55db8b44088198a2d72da798535bffb64fba5c', + currencies: ['0xf703620970dcb2f6c5a8eac1c446ec1abddb8191'], }, // Mainnet 1: { centrifugeRouter: '0x2F445BA946044C5F508a63eEaF7EAb673c69a1F4', router: '0x85bafcadea202258e3512ffbc3e2c9ee6ad56365', + currencies: [], }, 42161: { centrifugeRouter: '0x2F445BA946044C5F508a63eEaF7EAb673c69a1F4', router: '0x85bafcadea202258e3512ffbc3e2c9ee6ad56365', + currencies: [], }, 8453: { centrifugeRouter: '0xF35501E7fC4a076E744dbAFA883CED74CCF5009d', router: '0x30e34260b895cae34a1cfb185271628c53311cf3', + currencies: [], }, 42220: { centrifugeRouter: '0x5a00C4fF931f37202aD4Be1FDB297E9EDc1CBb33', router: '0xe4e34083a49df72e634121f32583c9ea59191cca', + currencies: [], }, } + +export type CurrencyMetadata = { + address: HexString + decimals: number + name: string + symbol: string + chainId: number + supportsPermit: boolean +} diff --git a/src/tests/Vault.test.ts b/src/tests/Vault.test.ts new file mode 100644 index 0000000..5003fb3 --- /dev/null +++ b/src/tests/Vault.test.ts @@ -0,0 +1,55 @@ +import { expect } from 'chai' +import { firstValueFrom, skip, skipWhile } from 'rxjs' +import sinon from 'sinon' +import { Pool } from '../Pool.js' +import { PoolNetwork } from '../PoolNetwork.js' +import { Vault } from '../Vault.js' +import { context } from './setup.js' + +const poolId = '2779829532' +const trancheId = '0xac6bffc5fd68f7772ceddec7b0a316ca' +const asset = '0x8503b4452bf6238cc76cdbee223b46d7196b1c93' +const vaultAddress = '0x05eb35c2e4fa21fb06d3fab92916191b254b3504' +const investorA = '0x423420Ae467df6e90291fd0252c0A8a637C1e03f' + +describe('Vault', () => { + let vault: Vault + beforeEach(() => { + const { centrifuge } = context + const pool = new Pool(centrifuge, poolId) + const poolNetwork = new PoolNetwork(centrifuge, pool, 11155111) + vault = new Vault(centrifuge, poolNetwork, trancheId, asset, vaultAddress) + }) + + it('get investment details for an investor', async () => { + const fetchSpy = sinon.spy(globalThis, 'fetch') + const investment = await vault.investment(investorA) + expect(investment.isAllowedToInvest).to.equal(true) + // Calls should get batched + expect(fetchSpy.getCalls().length).to.equal(4) + }) + + it('should place an invest order', async () => { + context.tenderlyFork.impersonateAddress = investorA + context.centrifuge.setSigner(context.tenderlyFork.signer) + const [result, investmentAfter] = await Promise.all([ + vault.placeInvestOrder(100_000_000n), + firstValueFrom(vault.investment(investorA).pipe(skipWhile((i) => i.pendingInvestCurrency !== 100_000_000n))), + ]) + expect(result.type).to.equal('TransactionConfirmed') + expect(investmentAfter.pendingInvestCurrency).to.equal(100_000_000n) + }) + + it('should cancel an order', async () => { + const investmentBefore = await vault.investment(investorA) + expect(investmentBefore.hasPendingCancelRedeemRequest).to.equal(false) + context.tenderlyFork.impersonateAddress = investorA + context.centrifuge.setSigner(context.tenderlyFork.signer) + const [result, investmentAfter] = await Promise.all([ + vault.cancelRedeemOrder(), + firstValueFrom(vault.investment(investorA).pipe(skip(1))), + ]) + expect(result.type).to.equal('TransactionConfirmed') + expect(investmentAfter.hasPendingCancelRedeemRequest).to.equal(true) + }) +}) From c73e82c0110aaaeee2bdf5ec0bf1b0cabb4e650d Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:03:50 +0100 Subject: [PATCH 40/53] comments --- src/Centrifuge.ts | 11 +++++++++++ src/index.ts | 1 + 2 files changed, 12 insertions(+) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index d3d890a..cacb5eb 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -135,6 +135,11 @@ export class Centrifuge { return this._query(null, () => of(new Account(this, address as any, chainId ?? this.config.defaultChain))) } + /** + * Get the metadata for an ERC20 token + * @param address - The token address + * @param chainId - The chain ID + */ currency(address: string, chainId?: number): Query { const curAddress = address.toLowerCase() const cid = chainId ?? this.config.defaultChain @@ -165,6 +170,12 @@ export class Centrifuge { ) } + /** + * Get the balance of an ERC20 token for a given owner. + * @param currency - The token address + * @param owner - The owner address + * @param chainId - The chain ID + */ balance(currency: string, owner: string, chainId?: number) { const address = owner.toLowerCase() const cid = chainId ?? this.config.defaultChain diff --git a/src/index.ts b/src/index.ts index dff7167..4046ac3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,5 +4,6 @@ export * from './PoolNetwork.js' export * from './types/index.js' export * from './types/query.js' export * from './types/transaction.js' +export * from './Vault.js' export default Centrifuge From de540145506eae2e7af862452289130a4c3c0785 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:59:55 +0100 Subject: [PATCH 41/53] tests --- src/Vault.ts | 72 ++++++++++---- src/abi/Currency.abi.json | 3 +- src/abi/RestrictionManager.abi.json | 22 +++++ src/abi/index.ts | 2 + src/tests/Vault.test.ts | 145 ++++++++++++++++++++++++++-- src/tests/tenderly.ts | 2 +- 6 files changed, 219 insertions(+), 27 deletions(-) create mode 100644 src/abi/RestrictionManager.abi.json diff --git a/src/Vault.ts b/src/Vault.ts index f8b31cd..8928a42 100644 --- a/src/Vault.ts +++ b/src/Vault.ts @@ -45,7 +45,7 @@ export class Vault extends Entity { * @internal */ _estimate() { - return this._root._query(['estimate'], () => + return this._root._query(['estimate', this.chainId], () => defer(() => { const bytes = toHex(new Uint8Array([0x12])) const { centrifugeRouter } = lpConfig[this.chainId]! @@ -76,6 +76,27 @@ export class Vault extends Entity { ) } + /** + * Get the contract address of the restriction mananger. + * @internal + */ + _restrictionManager() { + return this._query(['restrictionManager'], () => + this._share().pipe( + switchMap((share) => + defer( + () => + this._root.getClient(this.chainId)!.readContract({ + address: share, + abi: ABI.Currency, + functionName: 'hook', + }) as Promise + ) + ) + ) + ) + } + /** * Get the details of the investment currency. */ @@ -136,8 +157,13 @@ export class Vault extends Entity { investment(investor: string) { const address = investor.toLowerCase() as HexString return this._query(['investment', address], () => - combineLatest([this.investmentCurrency(), this.shareCurrency(), this.network._investmentManager()]).pipe( - switchMap(([investmentCurrency, shareCurrency, investmentManagerAddress]) => + combineLatest([ + this.investmentCurrency(), + this.shareCurrency(), + this.network._investmentManager(), + this._restrictionManager(), + ]).pipe( + switchMap(([investmentCurrency, shareCurrency, investmentManagerAddress, restrictionManagerAddress]) => combineLatest([ this._root.balance(investmentCurrency.address, address, this.chainId), this._root.balance(shareCurrency.address, address, this.chainId), @@ -167,12 +193,11 @@ export class Vault extends Entity { , pendingInvest, pendingRedeem, - claimableCancelDepositCurrency, + claimableCancelInvestCurrency, claimableCancelRedeemShares, - hasPendingCancelDepositRequest, + hasPendingCancelInvestRequest, hasPendingCancelRedeemRequest, ] = investment - console.log('pendingInvest', pendingInvest) return { isAllowedToInvest, claimableInvestShares: maxMint, @@ -181,9 +206,9 @@ export class Vault extends Entity { claimableRedeemSharesEquivalent: maxRedeem, pendingInvestCurrency: pendingInvest, pendingRedeemShares: pendingRedeem, - claimableCancelDepositCurrency, + claimableCancelInvestCurrency, claimableCancelRedeemShares, - hasPendingCancelDepositRequest, + hasPendingCancelInvestRequest, hasPendingCancelRedeemRequest, investmentCurrency, shareCurrency, @@ -192,9 +217,10 @@ export class Vault extends Entity { repeatOnEvents( this._root, { - address: this.address, - abi: ABI.LiquidityPool, + address: [this.address, restrictionManagerAddress], + abi: [ABI.LiquidityPool, ABI.RestrictionManager], eventName: [ + 'UpdateMember', 'CancelDepositClaim', 'CancelDepositClaimable', 'CancelDepositRequest', @@ -212,10 +238,13 @@ export class Vault extends Entity { console.log('events', events) return events.some( (event) => - event.args.receiver === investor || - event.args.controller === investor || - event.args.sender === investor || - event.args.owner === investor + event.args.receiver?.toLowerCase() === address || + event.args.controller?.toLowerCase() === address || + event.args.sender?.toLowerCase() === address || + event.args.owner?.toLowerCase() === address || + // UpdateMember event + (event.args.user?.toLowerCase() === address && + event.args.token?.toLowerCase() === shareCurrency.address) ) }, }, @@ -250,13 +279,13 @@ export class Vault extends Entity { isAllowedToInvest, pendingInvestCurrency, } = investment - const supportsPermit = investmentCurrency.supportsPermit && 'send' in signer + const supportsPermit = investmentCurrency.supportsPermit && 'send' in signer // eth-permit uses the deprecated send method const needsApproval = investmentCurrencyAllowance < investAmount if (!isAllowedToInvest) throw new Error('Not allowed to invest') if (investAmount === 0n && pendingInvestCurrency === 0n) throw new Error('No order to cancel') if (investAmount > investmentCurrencyBalance) throw new Error('Insufficient balance') - if (pendingInvestCurrency > 0n) throw new Error('Cannot change order') + if (pendingInvestCurrency > 0n && investAmount > 0n) throw new Error('Cannot change order') if (investAmount === 0n) { yield* doTransaction('Cancel Invest Order', publicClient, () => @@ -354,6 +383,7 @@ export class Vault extends Entity { if (shares === 0n && pendingRedeemShares === 0n) throw new Error('No order to cancel') if (shares > shareBalance) throw new Error('Insufficient balance') + if (pendingRedeemShares > 0n && shares > 0n) throw new Error('Cannot change order') if (shares === 0n) { yield* doTransaction('Cancel Redeem Order', publicClient, () => walletClient.writeContract({ @@ -370,7 +400,7 @@ export class Vault extends Entity { walletClient.writeContract({ address: centrifugeRouter, abi: ABI.CentrifugeRouter, - functionName: 'redeem', + functionName: 'requestRedeem', args: [self.address, shares, signingAddress, signingAddress, estimate], value: estimate, }) @@ -395,13 +425,17 @@ export class Vault extends Entity { const investment = await this.investment(signingAddress) const receiverAddress = receiver || signingAddress const functionName = - investment.claimableCancelDepositCurrency > 0n + investment.claimableCancelInvestCurrency > 0n ? 'claimCancelDepositRequest' : investment.claimableCancelRedeemShares > 0n ? 'claimCancelRedeemRequest' : investment.claimableInvestShares > 0n ? 'claimDeposit' - : 'claimRedeem' + : investment.claimableRedeemCurrency > 0n + ? 'claimRedeem' + : '' + + if (!functionName) throw new Error('No claimable funds') return walletClient.writeContract({ address: centrifugeRouter, diff --git a/src/abi/Currency.abi.json b/src/abi/Currency.abi.json index 2a0a03c..56cbb5e 100644 --- a/src/abi/Currency.abi.json +++ b/src/abi/Currency.abi.json @@ -9,5 +9,6 @@ "function decimals() view returns (uint8)", "function name() view returns (string)", "function symbol() view returns (string)", - "function checkTransferRestriction(address, address, uint) view returns (bool)" + "function checkTransferRestriction(address, address, uint) view returns (bool)", + "function hook() view returns (address)" ] diff --git a/src/abi/RestrictionManager.abi.json b/src/abi/RestrictionManager.abi.json new file mode 100644 index 0000000..72bb9aa --- /dev/null +++ b/src/abi/RestrictionManager.abi.json @@ -0,0 +1,22 @@ +[ + "event Deny(address indexed user)", + "event Freeze(address indexed token, address indexed user)", + "event Rely(address indexed user)", + "event Unfreeze(address indexed token, address indexed user)", + "event UpdateMember(address indexed token, address indexed user, uint64 validUntil)", + "function FREEZE_BIT() view returns (uint8)", + "function checkERC20Transfer(address from, address to, uint256, (bytes16 from, bytes16 to) hookData) view returns (bool)", + "function deny(address user)", + "function freeze(address token, address user)", + "function isFrozen(address token, address user) view returns (bool)", + "function isMember(address token, address user) view returns (bool isValid, uint64 validUntil)", + "function onERC20AuthTransfer(address, address, address, uint256, (bytes16 from, bytes16 to)) pure returns (bytes4)", + "function onERC20Transfer(address from, address to, uint256 value, (bytes16 from, bytes16 to) hookData) returns (bytes4)", + "function rely(address user)", + "function root() view returns (address)", + "function supportsInterface(bytes4 interfaceId) pure returns (bool)", + "function unfreeze(address token, address user)", + "function updateMember(address token, address user, uint64 validUntil)", + "function updateRestriction(address token, bytes update)", + "function wards(address) view returns (uint256)" +] diff --git a/src/abi/index.ts b/src/abi/index.ts index 506cb4d..43bf37a 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -5,11 +5,13 @@ import Gateway from './Gateway.abi.json' assert { type: 'json' } import InvestmentManager from './InvestmentManager.abi.json' assert { type: 'json' } import LiquidityPool from './LiquidityPool.abi.json' assert { type: 'json' } import PoolManager from './PoolManager.abi.json' assert { type: 'json' } +import RestrictionManager from './RestrictionManager.abi.json' assert { type: 'json' } import Router from './Router.abi.json' assert { type: 'json' } export const ABI = { CentrifugeRouter: parseAbi(CentrifugeRouter), Currency: parseAbi(Currency), + RestrictionManager: parseAbi(RestrictionManager), Gateway: parseAbi(Gateway), InvestmentManager: parseAbi(InvestmentManager), LiquidityPool: parseAbi(LiquidityPool), diff --git a/src/tests/Vault.test.ts b/src/tests/Vault.test.ts index 5003fb3..6ed87f4 100644 --- a/src/tests/Vault.test.ts +++ b/src/tests/Vault.test.ts @@ -1,6 +1,7 @@ import { expect } from 'chai' -import { firstValueFrom, skip, skipWhile } from 'rxjs' +import { firstValueFrom, lastValueFrom, skip, skipWhile, tap, toArray } from 'rxjs' import sinon from 'sinon' +import { ABI } from '../abi/index.js' import { Pool } from '../Pool.js' import { PoolNetwork } from '../PoolNetwork.js' import { Vault } from '../Vault.js' @@ -10,7 +11,17 @@ const poolId = '2779829532' const trancheId = '0xac6bffc5fd68f7772ceddec7b0a316ca' const asset = '0x8503b4452bf6238cc76cdbee223b46d7196b1c93' const vaultAddress = '0x05eb35c2e4fa21fb06d3fab92916191b254b3504' + +// Active investor with a pending redeem order const investorA = '0x423420Ae467df6e90291fd0252c0A8a637C1e03f' +// Permissioned investor with no orders +const investorB = '0xa076b817Fade13Ee72C495910eDCe1ed953F9930' // E2E Admin +// Investor with a claimable invest order +const investorC = '0x7fAbAa12da2E30650c841AC647e3567f942fcdf5' // E2E Borrower +// Non-permissioned investor +const investorD = '0x63892115da2e40f8135Abe99Dc5155dd552464F4' // E2E Nav Manager +// Investor with a claimable cancel deposit +const investorE = '0x655631E9F3d31a70DD6c9B4cFB5CfDe7445Fd0d2' // E2E Fee receiver describe('Vault', () => { let vault: Vault @@ -29,18 +40,117 @@ describe('Vault', () => { expect(fetchSpy.getCalls().length).to.equal(4) }) + it("should throw when placing an invest order larger than the users's balance", async () => { + context.tenderlyFork.impersonateAddress = investorB + context.centrifuge.setSigner(context.tenderlyFork.signer) + let error: Error | null = null + let wasAskedToSign = false + try { + await lastValueFrom(vault.placeInvestOrder(1000000000000000n).pipe(tap(() => (wasAskedToSign = true)))) + } catch (e: any) { + error = e + } + expect(error?.message).to.equal('Insufficient balance') + expect(wasAskedToSign).to.equal(false) + }) + + it('should throw when not allowed to invest', async () => { + context.tenderlyFork.impersonateAddress = investorD + context.centrifuge.setSigner(context.tenderlyFork.signer) + let error: Error | null = null + let wasAskedToSign = false + try { + await lastValueFrom(vault.placeInvestOrder(100000000n).pipe(tap(() => (wasAskedToSign = true)))) + } catch (e: any) { + error = e + } + expect(error?.message).to.equal('Not allowed to invest') + expect(wasAskedToSign).to.equal(false) + }) + it('should place an invest order', async () => { - context.tenderlyFork.impersonateAddress = investorA + context.tenderlyFork.impersonateAddress = investorB + context.centrifuge.setSigner(context.tenderlyFork.signer) + const [result, investmentAfter] = await Promise.all([ + lastValueFrom(vault.placeInvestOrder(100000000n).pipe(toArray())), + firstValueFrom(vault.investment(investorB).pipe(skipWhile((i) => i.pendingInvestCurrency !== 100000000n))), + ]) + expect(result[2]?.type).to.equal('TransactionConfirmed') + expect((result[2] as any).title).to.equal('Approve') + expect(result[5]?.type).to.equal('TransactionConfirmed') + expect((result[5] as any).title).to.equal('Invest') + expect(investmentAfter.pendingInvestCurrency).to.equal(100000000n) + }) + + it('should cancel an invest order', async () => { + const investmentBefore = await vault.investment(investorB) + expect(investmentBefore.hasPendingCancelInvestRequest).to.equal(false) + context.tenderlyFork.impersonateAddress = investorB + context.centrifuge.setSigner(context.tenderlyFork.signer) + const [result, investmentAfter] = await Promise.all([ + vault.cancelInvestOrder(), + firstValueFrom(vault.investment(investorB).pipe(skip(1))), + ]) + expect(result.type).to.equal('TransactionConfirmed') + expect(investmentAfter.hasPendingCancelInvestRequest).to.equal(true) + }) + + it('should claim a processed cancellation', async () => { + const investmentBefore = await vault.investment(investorE) + expect(investmentBefore.claimableCancelInvestCurrency).to.equal(1234000000n) + context.tenderlyFork.impersonateAddress = investorE context.centrifuge.setSigner(context.tenderlyFork.signer) const [result, investmentAfter] = await Promise.all([ - vault.placeInvestOrder(100_000_000n), - firstValueFrom(vault.investment(investorA).pipe(skipWhile((i) => i.pendingInvestCurrency !== 100_000_000n))), + vault.claim(), + firstValueFrom(vault.investment(investorE).pipe(skipWhile((i) => i.claimableCancelInvestCurrency !== 0n))), ]) expect(result.type).to.equal('TransactionConfirmed') - expect(investmentAfter.pendingInvestCurrency).to.equal(100_000_000n) + expect(investmentAfter.claimableCancelInvestCurrency).to.equal(0n) }) - it('should cancel an order', async () => { + it('should throw when trying to cancel a non-existing order', async () => { + context.tenderlyFork.impersonateAddress = investorB + context.centrifuge.setSigner(context.tenderlyFork.signer) + let thrown = false + let wasAskedToSign = false + try { + await lastValueFrom(vault.cancelRedeemOrder().pipe(tap(() => (wasAskedToSign = true)))) + } catch { + thrown = true + } + expect(thrown).to.equal(true) + expect(wasAskedToSign).to.equal(false) + }) + + it('should claim an executed order', async () => { + const investmentBefore = await vault.investment(investorC) + expect(investmentBefore.claimableInvestShares).to.equal(939254224n) + expect(investmentBefore.claimableInvestCurrencyEquivalent).to.equal(999999999n) + context.tenderlyFork.impersonateAddress = investorC + context.centrifuge.setSigner(context.tenderlyFork.signer) + const [result, investmentAfter] = await Promise.all([ + vault.claim(), + firstValueFrom(vault.investment(investorC).pipe(skipWhile((i) => i.claimableInvestShares !== 0n))), + ]) + expect(result.type).to.equal('TransactionConfirmed') + expect(investmentAfter.claimableInvestShares).to.equal(0n) + expect(investmentAfter.claimableInvestCurrencyEquivalent).to.equal(0n) + expect(investmentAfter.shareBalance).to.equal(939254224n) + }) + + it('should place a redeem order', async () => { + context.tenderlyFork.impersonateAddress = investorC + context.centrifuge.setSigner(context.tenderlyFork.signer) + const [result, investmentAfter] = await Promise.all([ + lastValueFrom(vault.placeRedeemOrder(939254224n).pipe(toArray())), + firstValueFrom(vault.investment(investorC).pipe(skipWhile((i) => i.pendingRedeemShares !== 939254224n))), + ]) + expect(result[2]?.type).to.equal('TransactionConfirmed') + expect((result[2] as any).title).to.equal('Redeem') + expect(investmentAfter.pendingRedeemShares).to.equal(939254224n) + }) + + it('should cancel a redeem order', async () => { const investmentBefore = await vault.investment(investorA) expect(investmentBefore.hasPendingCancelRedeemRequest).to.equal(false) context.tenderlyFork.impersonateAddress = investorA @@ -52,4 +162,27 @@ describe('Vault', () => { expect(result.type).to.equal('TransactionConfirmed') expect(investmentAfter.hasPendingCancelRedeemRequest).to.equal(true) }) + + it('should refetch investment details after a user is added', async () => { + const [poolManager, restrictionManager, investmentBefore] = await Promise.all([ + vault.network._poolManager(), + vault._restrictionManager(), + vault.investment(investorD), + ]) + expect(investmentBefore.isAllowedToInvest).to.equal(false) + context.tenderlyFork.impersonateAddress = poolManager + context.centrifuge.setSigner(context.tenderlyFork.signer) + const [, investmentAfter] = await Promise.all([ + context.centrifuge._transact('Add Investor', ({ walletClient }) => + walletClient.writeContract({ + address: restrictionManager, + abi: ABI.RestrictionManager, + functionName: 'updateMember', + args: [investmentBefore.shareCurrency.address, investorD, Math.floor(Date.now() / 1000) + 100000], + }) + ), + firstValueFrom(vault.investment(investorD).pipe(skip(1))), + ]) + expect(investmentAfter.isAllowedToInvest).to.equal(true) + }) }) diff --git a/src/tests/tenderly.ts b/src/tests/tenderly.ts index 791186e..049485e 100644 --- a/src/tests/tenderly.ts +++ b/src/tests/tenderly.ts @@ -201,7 +201,7 @@ export class TenderlyFork { display_name: `Centrifuge Sepolia Fork ${timestamp}`, fork_config: { network_id: this.chain.id, - block_number: '6924285', + block_number: '7116950', }, virtual_network_config: { chain_config: { From be5510c98693a28178fa788e7ca21cfbad6b4c21 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:34:53 +0100 Subject: [PATCH 42/53] big number types --- src/Centrifuge.ts | 12 +- src/Pool.ts | 15 ++ src/Vault.ts | 128 +++++++------- yarn.lock | 431 ++++++++++++++++++++++++---------------------- 4 files changed, 319 insertions(+), 267 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 6c2ee34..e41f634 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -38,6 +38,7 @@ import { Pool } from './Pool.js' import type { HexString } from './types/index.js' import type { CentrifugeQueryOptions, Query } from './types/query.js' import type { OperationStatus, Signer, Transaction, TransactionCallbackParams } from './types/transaction.js' +import { Currency } from './utils/BigInt.js' import { hashKey } from './utils/query.js' import { makeThenable, repeatOnEvents, shareReplayWithDelayedReset } from './utils/rx.js' import { doTransaction, isLocalAccount } from './utils/transaction.js' @@ -183,15 +184,16 @@ export class Centrifuge { const cid = chainId ?? this.config.defaultChain return this._query(['balance', currency, owner, cid], () => { return this.currency(currency, cid).pipe( - switchMap(() => - defer( - () => - this.getClient(cid)!.readContract({ + switchMap((currencyMeta) => + defer(() => + this.getClient(cid)! + .readContract({ address: currency as any, abi: ABI.Currency, functionName: 'balanceOf', args: [address], - }) as Promise + }) + .then((val: any) => new Currency(val, currencyMeta.decimals)) ).pipe( repeatOnEvents( this, diff --git a/src/Pool.ts b/src/Pool.ts index 834bda9..11eb865 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -49,6 +49,21 @@ export class Pool extends Entity { }) } + /** + * Get a specific network where a pool can potentially be deployed. + */ + network(chainId: number) { + return this._query(null, () => { + return this.networks().pipe( + map((networks) => { + const network = networks.find((network) => network.chainId === chainId) + if (!network) throw new Error(`Network ${chainId} not found`) + return network + }) + ) + }) + } + /** * Get the networks where a pool is active. It doesn't mean that any vaults are deployed there necessarily. */ diff --git a/src/Vault.ts b/src/Vault.ts index 8928a42..401c7f1 100644 --- a/src/Vault.ts +++ b/src/Vault.ts @@ -7,6 +7,7 @@ import { PoolNetwork } from './PoolNetwork.js' import { ABI } from './abi/index.js' import { lpConfig } from './config/lp.js' import type { HexString } from './types/index.js' +import { Currency, Token } from './utils/BigInt.js' import { repeatOnEvents } from './utils/rx.js' import { doSignMessage, doTransaction, signPermit, type Permit } from './utils/transaction.js' @@ -119,32 +120,38 @@ export class Vault extends Entity { allowance(owner: string) { const address = owner.toLowerCase() return this._query(['allowance', address], () => - defer( - () => - this._root.getClient(this.chainId)!.readContract({ - address: this._asset, - abi: ABI.Currency, - functionName: 'allowance', - args: [address, lpConfig[this.chainId]!.centrifugeRouter], - }) as Promise - ).pipe( - repeatOnEvents( - this._root, - { - address: this._asset, - abi: ABI.Currency, - eventName: ['Approval', 'Transfer'], - filter: (events) => { - return events.some((event) => { - return ( - event.args.owner?.toLowerCase() === address || - event.args.spender?.toLowerCase() === this._asset || - event.args.from?.toLowerCase() === address - ) + this.investmentCurrency().pipe( + switchMap((currency) => + defer(() => + this._root + .getClient(this.chainId)! + .readContract({ + address: this._asset, + abi: ABI.Currency, + functionName: 'allowance', + args: [address, lpConfig[this.chainId]!.centrifugeRouter], }) - }, - }, - this.chainId + .then((val: any) => new Currency(val, currency.decimals)) + ).pipe( + repeatOnEvents( + this._root, + { + address: this._asset, + abi: ABI.Currency, + eventName: ['Approval', 'Transfer'], + filter: (events) => { + return events.some((event) => { + return ( + event.args.owner?.toLowerCase() === address || + event.args.spender?.toLowerCase() === this._asset || + event.args.from?.toLowerCase() === address + ) + }) + }, + }, + this.chainId + ) + ) ) ) ) @@ -200,14 +207,14 @@ export class Vault extends Entity { ] = investment return { isAllowedToInvest, - claimableInvestShares: maxMint, - claimableInvestCurrencyEquivalent: maxDeposit, - claimableRedeemCurrency: maxWithdraw, - claimableRedeemSharesEquivalent: maxRedeem, - pendingInvestCurrency: pendingInvest, - pendingRedeemShares: pendingRedeem, - claimableCancelInvestCurrency, - claimableCancelRedeemShares, + claimableInvestShares: new Token(maxMint, shareCurrency.decimals), + claimableInvestCurrencyEquivalent: new Currency(maxDeposit, investmentCurrency.decimals), + claimableRedeemCurrency: new Currency(maxWithdraw, investmentCurrency.decimals), + claimableRedeemSharesEquivalent: new Token(maxRedeem, shareCurrency.decimals), + pendingInvestCurrency: new Currency(pendingInvest, investmentCurrency.decimals), + pendingRedeemShares: new Token(pendingRedeem, shareCurrency.decimals), + claimableCancelInvestCurrency: new Currency(claimableCancelInvestCurrency, investmentCurrency.decimals), + claimableCancelRedeemShares: new Token(claimableCancelRedeemShares, shareCurrency.decimals), hasPendingCancelInvestRequest, hasPendingCancelRedeemRequest, investmentCurrency, @@ -255,7 +262,7 @@ export class Vault extends Entity { ), map(([currencyBalance, shareBalance, allowance, investment]) => ({ ...investment, - shareBalance, + shareBalance: new Token(shareBalance.toBigInt(), investment.shareCurrency.decimals), investmentCurrencyBalance: currencyBalance, investmentCurrencyAllowance: allowance, })) @@ -267,11 +274,12 @@ export class Vault extends Entity { * Place an order to invest funds in the vault. If the amount is 0, it will request to cancel an open order. * @param investAmount - The amount to invest in the vault */ - placeInvestOrder(investAmount: bigint) { + placeInvestOrder(investAmount: bigint | number) { const self = this return this._transactSequence(async function* ({ walletClient, publicClient, signer, signingAddress }) { const { centrifugeRouter } = lpConfig[self.chainId]! const [estimate, investment] = await Promise.all([self._estimate(), self.investment(signingAddress)]) + const amount = new Currency(investAmount, investment.investmentCurrency.decimals) const { investmentCurrency, investmentCurrencyBalance, @@ -280,14 +288,14 @@ export class Vault extends Entity { pendingInvestCurrency, } = investment const supportsPermit = investmentCurrency.supportsPermit && 'send' in signer // eth-permit uses the deprecated send method - const needsApproval = investmentCurrencyAllowance < investAmount + const needsApproval = investmentCurrencyAllowance.lt(amount) if (!isAllowedToInvest) throw new Error('Not allowed to invest') - if (investAmount === 0n && pendingInvestCurrency === 0n) throw new Error('No order to cancel') - if (investAmount > investmentCurrencyBalance) throw new Error('Insufficient balance') - if (pendingInvestCurrency > 0n && investAmount > 0n) throw new Error('Cannot change order') + if (amount.isZero() && pendingInvestCurrency.isZero()) throw new Error('No order to cancel') + if (amount.gt(investmentCurrencyBalance)) throw new Error('Insufficient balance') + if (pendingInvestCurrency.gt(0n) && amount.gt(0n)) throw new Error('Cannot change order') - if (investAmount === 0n) { + if (amount.isZero()) { yield* doTransaction('Cancel Invest Order', publicClient, () => walletClient.writeContract({ address: centrifugeRouter, @@ -311,7 +319,7 @@ export class Vault extends Entity { signingAddress, investmentCurrency.address, centrifugeRouter, - investAmount + amount.toBigInt() ) ) } else { @@ -320,7 +328,7 @@ export class Vault extends Entity { address: investmentCurrency.address, abi: ABI.Currency, functionName: 'approve', - args: [centrifugeRouter, investAmount], + args: [centrifugeRouter, amount], }) ) } @@ -329,7 +337,7 @@ export class Vault extends Entity { const enableData = encodeFunctionData({ abi: ABI.CentrifugeRouter, functionName: 'enableLockDepositRequest', - args: [self.address, investAmount], + args: [self.address, amount], }) const requestData = encodeFunctionData({ abi: ABI.CentrifugeRouter, @@ -344,7 +352,7 @@ export class Vault extends Entity { args: [ investmentCurrency.address, centrifugeRouter, - investAmount.toString(), + amount.toString(), permit.deadline, permit.v, permit.r, @@ -374,17 +382,18 @@ export class Vault extends Entity { * Place an order to redeem funds from the vault. If the amount is 0, it will request to cancel an open order. * @param shares - The amount of shares to redeem */ - placeRedeemOrder(shares: bigint) { + placeRedeemOrder(shares: bigint | number) { const self = this return this._transactSequence(async function* ({ walletClient, publicClient, signingAddress }) { const { centrifugeRouter } = lpConfig[self.chainId]! const [estimate, investment] = await Promise.all([self._estimate(), self.investment(signingAddress)]) const { shareBalance, pendingRedeemShares } = investment + const amount = new Token(shares, investment.shareCurrency.decimals) - if (shares === 0n && pendingRedeemShares === 0n) throw new Error('No order to cancel') - if (shares > shareBalance) throw new Error('Insufficient balance') - if (pendingRedeemShares > 0n && shares > 0n) throw new Error('Cannot change order') - if (shares === 0n) { + if (amount.isZero() && pendingRedeemShares.isZero()) throw new Error('No order to cancel') + if (amount.gt(shareBalance)) throw new Error('Insufficient balance') + if (pendingRedeemShares.gt(0n) && amount.gt(0n)) throw new Error('Cannot change order') + if (amount.isZero()) { yield* doTransaction('Cancel Redeem Order', publicClient, () => walletClient.writeContract({ address: centrifugeRouter, @@ -401,7 +410,7 @@ export class Vault extends Entity { address: centrifugeRouter, abi: ABI.CentrifugeRouter, functionName: 'requestRedeem', - args: [self.address, shares, signingAddress, signingAddress, estimate], + args: [self.address, amount.toBigInt(), signingAddress, signingAddress, estimate], value: estimate, }) ) @@ -424,16 +433,15 @@ export class Vault extends Entity { const { centrifugeRouter } = lpConfig[this.chainId]! const investment = await this.investment(signingAddress) const receiverAddress = receiver || signingAddress - const functionName = - investment.claimableCancelInvestCurrency > 0n - ? 'claimCancelDepositRequest' - : investment.claimableCancelRedeemShares > 0n - ? 'claimCancelRedeemRequest' - : investment.claimableInvestShares > 0n - ? 'claimDeposit' - : investment.claimableRedeemCurrency > 0n - ? 'claimRedeem' - : '' + const functionName = investment.claimableCancelInvestCurrency.gt(0n) + ? 'claimCancelDepositRequest' + : investment.claimableCancelRedeemShares.gt(0n) + ? 'claimCancelRedeemRequest' + : investment.claimableInvestShares.gt(0n) + ? 'claimDeposit' + : investment.claimableRedeemCurrency.gt(0n) + ? 'claimRedeem' + : '' if (!functionName) throw new Error('No claimable funds') diff --git a/yarn.lock b/yarn.lock index edea16a..909a45d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,7 +5,7 @@ __metadata: version: 8 cacheKey: 10c0 -"@adraffy/ens-normalize@npm:1.11.0": +"@adraffy/ens-normalize@npm:^1.10.1": version: 1.11.0 resolution: "@adraffy/ens-normalize@npm:1.11.0" checksum: 10c0/5111d0f1a273468cb5661ed3cf46ee58de8f32f84e2ebc2365652e66c1ead82649df94c736804e2b9cfa831d30ef24e1cc3575d970dbda583416d3a98d8870a6 @@ -53,44 +53,44 @@ __metadata: linkType: hard "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": - version: 4.4.0 - resolution: "@eslint-community/eslint-utils@npm:4.4.0" + version: 4.4.1 + resolution: "@eslint-community/eslint-utils@npm:4.4.1" dependencies: - eslint-visitor-keys: "npm:^3.3.0" + eslint-visitor-keys: "npm:^3.4.3" peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10c0/7e559c4ce59cd3a06b1b5a517b593912e680a7f981ae7affab0d01d709e99cd5647019be8fafa38c350305bc32f1f7d42c7073edde2ab536c745e365f37b607e + checksum: 10c0/2aa0ac2fc50ff3f234408b10900ed4f1a0b19352f21346ad4cc3d83a1271481bdda11097baa45d484dd564c895e0762a27a8240be7a256b3ad47129e96528252 languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.11.0": - version: 4.11.1 - resolution: "@eslint-community/regexpp@npm:4.11.1" - checksum: 10c0/fbcc1cb65ef5ed5b92faa8dc542e035269065e7ebcc0b39c81a4fe98ad35cfff20b3c8df048641de15a7757e07d69f85e2579c1a5055f993413ba18c055654f8 +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1": + version: 4.12.1 + resolution: "@eslint-community/regexpp@npm:4.12.1" + checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6 languageName: node linkType: hard -"@eslint/config-array@npm:^0.18.0": - version: 0.18.0 - resolution: "@eslint/config-array@npm:0.18.0" +"@eslint/config-array@npm:^0.19.0": + version: 0.19.0 + resolution: "@eslint/config-array@npm:0.19.0" dependencies: "@eslint/object-schema": "npm:^2.1.4" debug: "npm:^4.3.1" minimatch: "npm:^3.1.2" - checksum: 10c0/0234aeb3e6b052ad2402a647d0b4f8a6aa71524bafe1adad0b8db1dfe94d7f5f26d67c80f79bb37ac61361a1d4b14bb8fb475efe501de37263cf55eabb79868f + checksum: 10c0/def23c6c67a8f98dc88f1b87e17a5668e5028f5ab9459661aabfe08e08f2acd557474bbaf9ba227be0921ae4db232c62773dbb7739815f8415678eb8f592dbf5 languageName: node linkType: hard -"@eslint/core@npm:^0.7.0": - version: 0.7.0 - resolution: "@eslint/core@npm:0.7.0" - checksum: 10c0/3cdee8bc6cbb96ac6103d3ead42e59830019435839583c9eb352b94ed558bd78e7ffad5286dc710df21ec1e7bd8f52aa6574c62457a4dd0f01f3736fa4a7d87a +"@eslint/core@npm:^0.9.0": + version: 0.9.0 + resolution: "@eslint/core@npm:0.9.0" + checksum: 10c0/6d8e8e0991cef12314c49425d8d2d9394f5fb1a36753ff82df7c03185a4646cb7c8736cf26638a4a714782cedf4b23cfc17667d282d3e5965b3920a0e7ce20d4 languageName: node linkType: hard -"@eslint/eslintrc@npm:^3.1.0": - version: 3.1.0 - resolution: "@eslint/eslintrc@npm:3.1.0" +"@eslint/eslintrc@npm:^3.2.0": + version: 3.2.0 + resolution: "@eslint/eslintrc@npm:3.2.0" dependencies: ajv: "npm:^6.12.4" debug: "npm:^4.3.2" @@ -101,14 +101,14 @@ __metadata: js-yaml: "npm:^4.1.0" minimatch: "npm:^3.1.2" strip-json-comments: "npm:^3.1.1" - checksum: 10c0/5b7332ed781edcfc98caa8dedbbb843abfb9bda2e86538529c843473f580e40c69eb894410eddc6702f487e9ee8f8cfa8df83213d43a8fdb549f23ce06699167 + checksum: 10c0/43867a07ff9884d895d9855edba41acf325ef7664a8df41d957135a81a477ff4df4196f5f74dc3382627e5cc8b7ad6b815c2cea1b58f04a75aced7c43414ab8b languageName: node linkType: hard -"@eslint/js@npm:9.13.0": - version: 9.13.0 - resolution: "@eslint/js@npm:9.13.0" - checksum: 10c0/672257bffe17777b8a98bd80438702904cc7a0b98b9c2e426a8a10929198b3553edf8a3fc20feed4133c02e7c8f7331a0ef1b23e5dab8e4469f7f1791beff1e0 +"@eslint/js@npm:9.15.0": + version: 9.15.0 + resolution: "@eslint/js@npm:9.15.0" + checksum: 10c0/56552966ab1aa95332f70d0e006db5746b511c5f8b5e0c6a9b2d6764ff6d964e0b2622731877cbc4e3f0e74c5b39191290d5f48147be19175292575130d499ab languageName: node linkType: hard @@ -119,29 +119,29 @@ __metadata: languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.2.0": - version: 0.2.1 - resolution: "@eslint/plugin-kit@npm:0.2.1" +"@eslint/plugin-kit@npm:^0.2.3": + version: 0.2.3 + resolution: "@eslint/plugin-kit@npm:0.2.3" dependencies: levn: "npm:^0.4.1" - checksum: 10c0/34b1ecb35df97b0adeb6a43366fc1b8aa1a54d23fc9753019277e80a7295724fddb547a795fd59c9eb56d690bbf0d76d7f2286cb0f5db367a86a763d5acbde5f + checksum: 10c0/89a8035976bb1780e3fa8ffe682df013bd25f7d102d991cecd3b7c297f4ce8c1a1b6805e76dd16465b5353455b670b545eff2b4ec3133e0eab81a5f9e99bd90f languageName: node linkType: hard -"@humanfs/core@npm:^0.19.0": - version: 0.19.0 - resolution: "@humanfs/core@npm:0.19.0" - checksum: 10c0/f87952d5caba6ae427a620eff783c5d0b6cef0cfc256dec359cdaa636c5f161edb8d8dad576742b3de7f0b2f222b34aad6870248e4b7d2177f013426cbcda232 +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 10c0/aa4e0152171c07879b458d0e8a704b8c3a89a8c0541726c6b65b81e84fd8b7564b5d6c633feadc6598307d34564bd53294b533491424e8e313d7ab6c7bc5dc67 languageName: node linkType: hard -"@humanfs/node@npm:^0.16.5": - version: 0.16.5 - resolution: "@humanfs/node@npm:0.16.5" +"@humanfs/node@npm:^0.16.6": + version: 0.16.6 + resolution: "@humanfs/node@npm:0.16.6" dependencies: - "@humanfs/core": "npm:^0.19.0" + "@humanfs/core": "npm:^0.19.1" "@humanwhocodes/retry": "npm:^0.3.0" - checksum: 10c0/41c365ab09e7c9eaeed373d09243195aef616d6745608a36fc3e44506148c28843872f85e69e2bf5f1e992e194286155a1c1cecfcece6a2f43875e37cd243935 + checksum: 10c0/8356359c9f60108ec204cbd249ecd0356667359b2524886b357617c4a7c3b6aace0fd5a369f63747b926a762a88f8a25bc066fa1778508d110195ce7686243e1 languageName: node linkType: hard @@ -152,13 +152,20 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/retry@npm:^0.3.0, @humanwhocodes/retry@npm:^0.3.1": +"@humanwhocodes/retry@npm:^0.3.0": version: 0.3.1 resolution: "@humanwhocodes/retry@npm:0.3.1" checksum: 10c0/f0da1282dfb45e8120480b9e2e275e2ac9bbe1cf016d046fdad8e27cc1285c45bb9e711681237944445157b430093412b4446c1ab3fc4bb037861b5904101d3b languageName: node linkType: hard +"@humanwhocodes/retry@npm:^0.4.1": + version: 0.4.1 + resolution: "@humanwhocodes/retry@npm:0.4.1" + checksum: 10c0/be7bb6841c4c01d0b767d9bb1ec1c9359ee61421ce8ba66c249d035c5acdfd080f32d55a5c9e859cdd7868788b8935774f65b2caf24ec0b7bd7bf333791f063b + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -197,7 +204,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.6.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:~1.6.0": +"@noble/curves@npm:1.6.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:^1.6.0, @noble/curves@npm:~1.6.0": version: 1.6.0 resolution: "@noble/curves@npm:1.6.0" dependencies: @@ -206,7 +213,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:~1.5.0": +"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.5.0": version: 1.5.0 resolution: "@noble/hashes@npm:1.5.0" checksum: 10c0/1b46539695fbfe4477c0822d90c881a04d4fa2921c08c552375b444a48cac9930cb1ee68de0a3c7859e676554d0f3771999716606dc4d8f826e414c11692cdd9 @@ -276,7 +283,7 @@ __metadata: languageName: node linkType: hard -"@scure/bip32@npm:1.5.0": +"@scure/bip32@npm:1.5.0, @scure/bip32@npm:^1.5.0": version: 1.5.0 resolution: "@scure/bip32@npm:1.5.0" dependencies: @@ -287,7 +294,7 @@ __metadata: languageName: node linkType: hard -"@scure/bip39@npm:1.4.0": +"@scure/bip39@npm:1.4.0, @scure/bip39@npm:^1.4.0": version: 1.4.0 resolution: "@scure/bip39@npm:1.4.0" dependencies: @@ -361,7 +368,7 @@ __metadata: languageName: node linkType: hard -"@types/chai@npm:*": +"@types/chai@npm:*, @types/chai@npm:^5.0.0": version: 5.0.1 resolution: "@types/chai@npm:5.0.1" dependencies: @@ -370,13 +377,6 @@ __metadata: languageName: node linkType: hard -"@types/chai@npm:^5.0.0": - version: 5.0.0 - resolution: "@types/chai@npm:5.0.0" - checksum: 10c0/fcce55f2bbb8485fc860a1dcbac17c1a685b598cfc91a55d37b65b1642b921cf736caa8cce9dcc530830d900f78ab95cf43db4e118db34a5176f252cacd9e1e8 - languageName: node - linkType: hard - "@types/deep-eql@npm:*": version: 4.0.2 resolution: "@types/deep-eql@npm:4.0.2" @@ -406,11 +406,11 @@ __metadata: linkType: hard "@types/node@npm:^22.7.8": - version: 22.7.8 - resolution: "@types/node@npm:22.7.8" + version: 22.9.1 + resolution: "@types/node@npm:22.9.1" dependencies: - undici-types: "npm:~6.19.2" - checksum: 10c0/3d3b3a2ec5a57ca4fd37b34dce415620993ca5f87cea2c728ffe73aa31446dbfe19c53171c478447bd7d78011ef4845a46ab2f0dc38e699cc75b3d100a60c690 + undici-types: "npm:~6.19.8" + checksum: 10c0/ea489ae603aa8874e4e88980aab6f2dad09c755da779c88dd142983bfe9609803c89415ca7781f723072934066f63daf2b3339ef084a8ad1a8079cf3958be243 languageName: node linkType: hard @@ -440,15 +440,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.11.0": - version: 8.11.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.11.0" +"@typescript-eslint/eslint-plugin@npm:8.15.0": + version: 8.15.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.15.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.11.0" - "@typescript-eslint/type-utils": "npm:8.11.0" - "@typescript-eslint/utils": "npm:8.11.0" - "@typescript-eslint/visitor-keys": "npm:8.11.0" + "@typescript-eslint/scope-manager": "npm:8.15.0" + "@typescript-eslint/type-utils": "npm:8.15.0" + "@typescript-eslint/utils": "npm:8.15.0" + "@typescript-eslint/visitor-keys": "npm:8.15.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -459,66 +459,68 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/be509f7bb0c0c596801059b06995a81a1c326cc6ac31d96a32f7b6b7d7b495f9bad4dc442aa6e923d22515e62c668d3c14695c68bd6e0be1d4bf72158b7fd2d6 + checksum: 10c0/90ef10cc7d37a81abec4f4a3ffdfc3a0da8e99d949e03c75437e96e8ab2e896e34b85ab64718690180a7712581031b8611c5d8e7666d6ed4d60b9ace834d58e3 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.11.0": - version: 8.11.0 - resolution: "@typescript-eslint/parser@npm:8.11.0" +"@typescript-eslint/parser@npm:8.15.0": + version: 8.15.0 + resolution: "@typescript-eslint/parser@npm:8.15.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.11.0" - "@typescript-eslint/types": "npm:8.11.0" - "@typescript-eslint/typescript-estree": "npm:8.11.0" - "@typescript-eslint/visitor-keys": "npm:8.11.0" + "@typescript-eslint/scope-manager": "npm:8.15.0" + "@typescript-eslint/types": "npm:8.15.0" + "@typescript-eslint/typescript-estree": "npm:8.15.0" + "@typescript-eslint/visitor-keys": "npm:8.15.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/e83f239fec60697083e5dcb1c8948340e783ea6e043fe9a65d557faef8882963b09d69aacd736eb8ab18a768769a7bbfc3de0f1251d4bba080613541acb0741c + checksum: 10c0/19c25aea0dc51faa758701a5319a89950fd30494d9d645db8ced84fb60714c5e7d4b51fc4ee8ccb07ddefec88c51ee307ee7e49addd6330ee8f3e7ee9ba329fc languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.11.0": - version: 8.11.0 - resolution: "@typescript-eslint/scope-manager@npm:8.11.0" +"@typescript-eslint/scope-manager@npm:8.15.0": + version: 8.15.0 + resolution: "@typescript-eslint/scope-manager@npm:8.15.0" dependencies: - "@typescript-eslint/types": "npm:8.11.0" - "@typescript-eslint/visitor-keys": "npm:8.11.0" - checksum: 10c0/0910da62d8ae261711dd9f89d5c7d8e96ff13c50054436256e5a661309229cb49e3b8189c9468d36b6c4d3f7cddd121519ea78f9b18c9b869a808834b079b2ea + "@typescript-eslint/types": "npm:8.15.0" + "@typescript-eslint/visitor-keys": "npm:8.15.0" + checksum: 10c0/c27dfdcea4100cc2d6fa967f857067cbc93155b55e648f9f10887a1b9372bb76cf864f7c804f3fa48d7868d9461cdef10bcea3dab7637d5337e8aa8042dc08b9 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.11.0": - version: 8.11.0 - resolution: "@typescript-eslint/type-utils@npm:8.11.0" +"@typescript-eslint/type-utils@npm:8.15.0": + version: 8.15.0 + resolution: "@typescript-eslint/type-utils@npm:8.15.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.11.0" - "@typescript-eslint/utils": "npm:8.11.0" + "@typescript-eslint/typescript-estree": "npm:8.15.0" + "@typescript-eslint/utils": "npm:8.15.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.3.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/b69e31c1599ceeb20c29052a4ddb33a554174a3a4c55ee37d90c9b8250af6ef978a0b9ddbeefef4e83d62c4caea1bfa2d8088527f397bde69fb4ab9b360d794a + checksum: 10c0/20f09c79c83b38a962cf7eff10d47a2c01bcc0bab7bf6d762594221cd89023ef8c7aec26751c47b524f53f5c8d38bba55a282529b3df82d5f5ab4350496316f9 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.11.0": - version: 8.11.0 - resolution: "@typescript-eslint/types@npm:8.11.0" - checksum: 10c0/5ccdd3eeee077a6fc8e7f4bc0e0cbc9327b1205a845253ec5c0c6c49ff915e853161df00c24a0ffb4b8ec745d3f153dd0e066400a021c844c026e31121f46699 +"@typescript-eslint/types@npm:8.15.0": + version: 8.15.0 + resolution: "@typescript-eslint/types@npm:8.15.0" + checksum: 10c0/84abc6fd954aff13822a76ac49efdcb90a55c0025c20eee5d8cebcfb68faff33b79bbc711ea524e0209cecd90c5ee3a5f92babc7083c081d3a383a0710264a41 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.11.0": - version: 8.11.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.11.0" +"@typescript-eslint/typescript-estree@npm:8.15.0": + version: 8.15.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.15.0" dependencies: - "@typescript-eslint/types": "npm:8.11.0" - "@typescript-eslint/visitor-keys": "npm:8.11.0" + "@typescript-eslint/types": "npm:8.15.0" + "@typescript-eslint/visitor-keys": "npm:8.15.0" debug: "npm:^4.3.4" fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" @@ -528,31 +530,34 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/b629ad3cd32b005d5c1d67c36958a418f8672efebea869399834f4f201ebf90b942165eebb5c9d9799dcabdc2cc26e5fabb00629f76b158847f42e1a491a75a6 + checksum: 10c0/3af5c129532db3575349571bbf64d32aeccc4f4df924ac447f5d8f6af8b387148df51965eb2c9b99991951d3dadef4f2509d7ce69bf34a2885d013c040762412 languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.11.0": - version: 8.11.0 - resolution: "@typescript-eslint/utils@npm:8.11.0" +"@typescript-eslint/utils@npm:8.15.0": + version: 8.15.0 + resolution: "@typescript-eslint/utils@npm:8.15.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.11.0" - "@typescript-eslint/types": "npm:8.11.0" - "@typescript-eslint/typescript-estree": "npm:8.11.0" + "@typescript-eslint/scope-manager": "npm:8.15.0" + "@typescript-eslint/types": "npm:8.15.0" + "@typescript-eslint/typescript-estree": "npm:8.15.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - checksum: 10c0/bb5bcc8d928a55b22298e76f834ea6a9fe125a9ffeb6ac23bee0258b3ed32f41e281888a3d0be226a05e1011bb3b70e42a71a40366acdefea6779131c46bc522 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/65743f51845a1f6fd2d21f66ca56182ba33e966716bdca73d30b7a67c294e47889c322de7d7b90ab0818296cd33c628e5eeeb03cec7ef2f76c47de7a453eeda2 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.11.0": - version: 8.11.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.11.0" +"@typescript-eslint/visitor-keys@npm:8.15.0": + version: 8.15.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.15.0" dependencies: - "@typescript-eslint/types": "npm:8.11.0" - eslint-visitor-keys: "npm:^3.4.3" - checksum: 10c0/7a5a49609fdc47e114fe59eee56393c90b122ec8e9520f90b0c5e189635ae1ccfa8e00108f641342c2c8f4637fe9d40c77927cf7c8248a3a660812cb4b7d0c08 + "@typescript-eslint/types": "npm:8.15.0" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10c0/02a954c3752c4328482a884eb1da06ca8fb72ae78ef28f1d854b18f3779406ed47263af22321cf3f65a637ec7584e5f483e34a263b5c8cec60ec85aebc263574 languageName: node linkType: hard @@ -563,7 +568,7 @@ __metadata: languageName: node linkType: hard -"abitype@npm:1.0.6": +"abitype@npm:1.0.6, abitype@npm:^1.0.6": version: 1.0.6 resolution: "abitype@npm:1.0.6" peerDependencies: @@ -596,12 +601,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.11.0, acorn@npm:^8.12.0, acorn@npm:^8.4.1": - version: 8.13.0 - resolution: "acorn@npm:8.13.0" +"acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.4.1": + version: 8.14.0 + resolution: "acorn@npm:8.14.0" bin: acorn: bin/acorn - checksum: 10c0/f35dd53d68177c90699f4c37d0bb205b8abe036d955d0eb011ddb7f14a81e6fd0f18893731c457c1b5bd96754683f4c3d80d9a5585ddecaa53cdf84e0b3d68f7 + checksum: 10c0/6d4ee461a7734b2f48836ee0fbb752903606e576cc100eb49340295129ca0b452f3ba91ddd4424a1d4406a98adfb2ebb6bd0ff4c49d7a0930c10e462719bbfd7 languageName: node linkType: hard @@ -976,26 +981,26 @@ __metadata: linkType: hard "cross-spawn@npm:^6.0.5": - version: 6.0.5 - resolution: "cross-spawn@npm:6.0.5" + version: 6.0.6 + resolution: "cross-spawn@npm:6.0.6" dependencies: nice-try: "npm:^1.0.4" path-key: "npm:^2.0.1" semver: "npm:^5.5.0" shebang-command: "npm:^1.2.0" which: "npm:^1.2.9" - checksum: 10c0/e05544722e9d7189b4292c66e42b7abeb21db0d07c91b785f4ae5fefceb1f89e626da2703744657b287e86dcd4af57b54567cef75159957ff7a8a761d9055012 + checksum: 10c0/bf61fb890e8635102ea9bce050515cf915ff6a50ccaa0b37a17dc82fded0fb3ed7af5478b9367b86baee19127ad86af4be51d209f64fd6638c0862dca185fe1d languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.5": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" dependencies: path-key: "npm:^3.1.0" shebang-command: "npm:^2.0.0" which: "npm:^2.0.1" - checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 languageName: node linkType: hard @@ -1176,8 +1181,8 @@ __metadata: linkType: hard "es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2": - version: 1.23.3 - resolution: "es-abstract@npm:1.23.3" + version: 1.23.5 + resolution: "es-abstract@npm:1.23.5" dependencies: array-buffer-byte-length: "npm:^1.0.1" arraybuffer.prototype.slice: "npm:^1.0.3" @@ -1194,7 +1199,7 @@ __metadata: function.prototype.name: "npm:^1.1.6" get-intrinsic: "npm:^1.2.4" get-symbol-description: "npm:^1.0.2" - globalthis: "npm:^1.0.3" + globalthis: "npm:^1.0.4" gopd: "npm:^1.0.1" has-property-descriptors: "npm:^1.0.2" has-proto: "npm:^1.0.3" @@ -1210,10 +1215,10 @@ __metadata: is-string: "npm:^1.0.7" is-typed-array: "npm:^1.1.13" is-weakref: "npm:^1.0.2" - object-inspect: "npm:^1.13.1" + object-inspect: "npm:^1.13.3" object-keys: "npm:^1.1.1" object.assign: "npm:^4.1.5" - regexp.prototype.flags: "npm:^1.5.2" + regexp.prototype.flags: "npm:^1.5.3" safe-array-concat: "npm:^1.1.2" safe-regex-test: "npm:^1.0.3" string.prototype.trim: "npm:^1.2.9" @@ -1225,7 +1230,7 @@ __metadata: typed-array-length: "npm:^1.0.6" unbox-primitive: "npm:^1.0.2" which-typed-array: "npm:^1.1.15" - checksum: 10c0/d27e9afafb225c6924bee9971a7f25f20c314f2d6cb93a63cada4ac11dcf42040896a6c22e5fb8f2a10767055ed4ddf400be3b1eb12297d281726de470b75666 + checksum: 10c0/1f6f91da9cf7ee2c81652d57d3046621d598654d1d1b05c1578bafe5c4c2d3d69513901679bdca2de589f620666ec21de337e4935cec108a4ed0871d5ef04a5d languageName: node linkType: hard @@ -1297,54 +1302,54 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^8.1.0": - version: 8.1.0 - resolution: "eslint-scope@npm:8.1.0" +"eslint-scope@npm:^8.2.0": + version: 8.2.0 + resolution: "eslint-scope@npm:8.2.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10c0/ae1df7accae9ea90465c2ded70f7064d6d1f2962ef4cc87398855c4f0b3a5ab01063e0258d954bb94b184f6759febe04c3118195cab5c51978a7229948ba2875 + checksum: 10c0/8d2d58e2136d548ac7e0099b1a90d9fab56f990d86eb518de1247a7066d38c908be2f3df477a79cf60d70b30ba18735d6c6e70e9914dca2ee515a729975d70d6 languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.3": +"eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.1.0": - version: 4.1.0 - resolution: "eslint-visitor-keys@npm:4.1.0" - checksum: 10c0/5483ef114c93a136aa234140d7aa3bd259488dae866d35cb0d0b52e6a158f614760a57256ac8d549acc590a87042cb40f6951815caa821e55dc4fd6ef4c722eb +"eslint-visitor-keys@npm:^4.2.0": + version: 4.2.0 + resolution: "eslint-visitor-keys@npm:4.2.0" + checksum: 10c0/2ed81c663b147ca6f578312919483eb040295bbab759e5a371953456c636c5b49a559883e2677112453728d66293c0a4c90ab11cab3428cf02a0236d2e738269 languageName: node linkType: hard "eslint@npm:^9.12.0": - version: 9.13.0 - resolution: "eslint@npm:9.13.0" + version: 9.15.0 + resolution: "eslint@npm:9.15.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" - "@eslint-community/regexpp": "npm:^4.11.0" - "@eslint/config-array": "npm:^0.18.0" - "@eslint/core": "npm:^0.7.0" - "@eslint/eslintrc": "npm:^3.1.0" - "@eslint/js": "npm:9.13.0" - "@eslint/plugin-kit": "npm:^0.2.0" - "@humanfs/node": "npm:^0.16.5" + "@eslint-community/regexpp": "npm:^4.12.1" + "@eslint/config-array": "npm:^0.19.0" + "@eslint/core": "npm:^0.9.0" + "@eslint/eslintrc": "npm:^3.2.0" + "@eslint/js": "npm:9.15.0" + "@eslint/plugin-kit": "npm:^0.2.3" + "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" - "@humanwhocodes/retry": "npm:^0.3.1" + "@humanwhocodes/retry": "npm:^0.4.1" "@types/estree": "npm:^1.0.6" "@types/json-schema": "npm:^7.0.15" ajv: "npm:^6.12.4" chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.2" + cross-spawn: "npm:^7.0.5" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^8.1.0" - eslint-visitor-keys: "npm:^4.1.0" - espree: "npm:^10.2.0" + eslint-scope: "npm:^8.2.0" + eslint-visitor-keys: "npm:^4.2.0" + espree: "npm:^10.3.0" esquery: "npm:^1.5.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" @@ -1359,7 +1364,6 @@ __metadata: minimatch: "npm:^3.1.2" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" - text-table: "npm:^0.2.0" peerDependencies: jiti: "*" peerDependenciesMeta: @@ -1367,18 +1371,18 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/d3577444152182a9d8ea8c6a6acb073d3a2773ad73a6b646f432746583ec4bfcd6a44fcc2e37d05d276984e583c46c2d289b3b981ca8f8b4052756a152341d19 + checksum: 10c0/d0d7606f36bfcccb1c3703d0a24df32067b207a616f17efe5fb1765a91d13f085afffc4fc97ecde4ab9c9f4edd64d9b4ce750e13ff7937a25074b24bee15b20f languageName: node linkType: hard -"espree@npm:^10.0.1, espree@npm:^10.2.0": - version: 10.2.0 - resolution: "espree@npm:10.2.0" +"espree@npm:^10.0.1, espree@npm:^10.3.0": + version: 10.3.0 + resolution: "espree@npm:10.3.0" dependencies: - acorn: "npm:^8.12.0" + acorn: "npm:^8.14.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^4.1.0" - checksum: 10c0/2b6bfb683e7e5ab2e9513949879140898d80a2d9867ea1db6ff5b0256df81722633b60a7523a7c614f05a39aeea159dd09ad2a0e90c0e218732fc016f9086215 + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10c0/272beeaca70d0a1a047d61baff64db04664a33d7cfb5d144f84bc8a5c6194c6c8ebe9cc594093ca53add88baa23e59b01e69e8a0160ab32eac570482e165c462 languageName: node linkType: hard @@ -1423,6 +1427,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:5.0.1": + version: 5.0.1 + resolution: "eventemitter3@npm:5.0.1" + checksum: 10c0/4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -1521,9 +1532,9 @@ __metadata: linkType: hard "flatted@npm:^3.2.9": - version: 3.3.1 - resolution: "flatted@npm:3.3.1" - checksum: 10c0/324166b125ee07d4ca9bcf3a5f98d915d5db4f39d711fba640a3178b959919aae1f7cfd8aabcfef5826ed8aa8a2aa14cc85b2d7d18ff638ddf4ae3df39573eaf + version: 3.3.2 + resolution: "flatted@npm:3.3.2" + checksum: 10c0/24cc735e74d593b6c767fe04f2ef369abe15b62f6906158079b9874bdb3ee5ae7110bb75042e70cd3f99d409d766f357caf78d5ecee9780206f5fdc5edbad334 languageName: node linkType: hard @@ -1601,17 +1612,18 @@ __metadata: version: 1.1.6 resolution: "function.prototype.name@npm:1.1.6" dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + functions-have-names: "npm:^1.2.3" + checksum: 10c0/9eae11294905b62cb16874adb4fc687927cda3162285e0ad9612e6a1d04934005d46907362ea9cdb7428edce05a2f2c3dabc3b2d21e9fd343e9bb278230ad94b languageName: node linkType: hard -"fs-minipass@npm:^3.0.0": - version: 3.0.3 - resolution: "fs-minipass@npm:3.0.3" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 +"functions-have-names@npm:^1.2.3": + version: 1.2.3 + resolution: "functions-have-names@npm:1.2.3" + checksum: 10c0/33e77fd29bddc2d9bb78ab3eb854c165909201f88c75faa8272e35899e2d35a8a642a15e7420ef945e1f64a9670d6aa3ec744106b2aa42be68ca5114025954ca languageName: node linkType: hard @@ -1701,13 +1713,13 @@ __metadata: linkType: hard "globals@npm:^15.11.0": - version: 15.11.0 - resolution: "globals@npm:15.11.0" - checksum: 10c0/861e39bb6bd9bd1b9f355c25c962e5eb4b3f0e1567cf60fa6c06e8c502b0ec8706b1cce055d69d84d0b7b8e028bec5418cf629a54e7047e116538d1c1c1a375c + version: 15.12.0 + resolution: "globals@npm:15.12.0" + checksum: 10c0/f34e0a1845b694f45188331742af9f488b07ba7440a06e9d2039fce0386fbbfc24afdbb9846ebdccd4092d03644e43081c49eb27b30f4b88e43af156e1c1dc34 languageName: node linkType: hard -"globalthis@npm:^1.0.3": +"globalthis@npm:^1.0.4": version: 1.0.4 resolution: "globalthis@npm:1.0.4" dependencies: @@ -2479,8 +2491,8 @@ __metadata: linkType: hard "mocha@npm:^10.7.3": - version: 10.7.3 - resolution: "mocha@npm:10.7.3" + version: 10.8.2 + resolution: "mocha@npm:10.8.2" dependencies: ansi-colors: "npm:^4.1.3" browser-stdout: "npm:^1.3.1" @@ -2505,7 +2517,7 @@ __metadata: bin: _mocha: bin/_mocha mocha: bin/mocha.js - checksum: 10c0/76a205905ec626262d903954daca31ba8e0dd4347092f627b98b8508dcdb5b30be62ec8f7a405fab3b2e691bdc099721c3291b330c3ee85b8ec40d3d179f8728 + checksum: 10c0/1f786290a32a1c234f66afe2bfcc68aa50fe9c7356506bd39cca267efb0b4714a63a0cb333815578d63785ba2fba058bf576c2512db73997c0cae0d659a88beb languageName: node linkType: hard @@ -2621,10 +2633,10 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.13.1": - version: 1.13.2 - resolution: "object-inspect@npm:1.13.2" - checksum: 10c0/b97835b4c91ec37b5fd71add84f21c3f1047d1d155d00c0fcd6699516c256d4fcc6ff17a1aced873197fe447f91a3964178fd2a67a1ee2120cdaf60e81a050b4 +"object-inspect@npm:^1.13.1, object-inspect@npm:^1.13.3": + version: 1.13.3 + resolution: "object-inspect@npm:1.13.3" + checksum: 10c0/cc3f15213406be89ffdc54b525e115156086796a515410a8d390215915db9f23c8eab485a06f1297402f440a33715fe8f71a528c1dcbad6e1a3bcaf5a46921d4 languageName: node linkType: hard @@ -2670,6 +2682,26 @@ __metadata: languageName: node linkType: hard +"ox@npm:0.1.2": + version: 0.1.2 + resolution: "ox@npm:0.1.2" + dependencies: + "@adraffy/ens-normalize": "npm:^1.10.1" + "@noble/curves": "npm:^1.6.0" + "@noble/hashes": "npm:^1.5.0" + "@scure/bip32": "npm:^1.5.0" + "@scure/bip39": "npm:^1.4.0" + abitype: "npm:^1.0.6" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/9d0615e9a95c316063587fe08dc268476e67429eea897598b2f69cb1509ac66739f888b0b9bc1cfd0b4bd2f1a3fd0af4d3e81d40ba0bf3abd53e36a6f5b21323 + languageName: node + linkType: hard + "p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" @@ -2890,7 +2922,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.2": +"regexp.prototype.flags@npm:^1.5.3": version: 1.5.3 resolution: "regexp.prototype.flags@npm:1.5.3" dependencies: @@ -3372,13 +3404,6 @@ __metadata: languageName: node linkType: hard -"text-table@npm:^0.2.0": - version: 0.2.0 - resolution: "text-table@npm:0.2.0" - checksum: 10c0/02805740c12851ea5982686810702e2f14369a5f4c5c40a836821e3eefc65ffeec3131ba324692a37608294b0fd8c1e55a2dd571ffed4909822787668ddbee5c - languageName: node - linkType: hard - "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -3389,11 +3414,11 @@ __metadata: linkType: hard "ts-api-utils@npm:^1.3.0": - version: 1.3.0 - resolution: "ts-api-utils@npm:1.3.0" + version: 1.4.0 + resolution: "ts-api-utils@npm:1.4.0" peerDependencies: typescript: ">=4.2.0" - checksum: 10c0/f54a0ba9ed56ce66baea90a3fa087a484002e807f28a8ccb2d070c75e76bde64bd0f6dce98b3802834156306050871b67eec325cb4e918015a360a3f0868c77c + checksum: 10c0/1b2bfa50ea52771d564bb143bb69010d25cda03ed573095fbac9b86f717012426443af6647e00e3db70fca60360482a30c1be7cf73c3521c321f6bf5e3594ea0 languageName: node linkType: hard @@ -3436,9 +3461,9 @@ __metadata: linkType: hard "tslib@npm:^2.1.0": - version: 2.8.0 - resolution: "tslib@npm:2.8.0" - checksum: 10c0/31e4d14dc1355e9b89e4d3c893a18abb7f90b6886b089c2da91224d0a7752c79f3ddc41bc1aa0a588ac895bd97bb99c5bc2bfdb2f86de849f31caeb3ba79bbe5 + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 languageName: node linkType: hard @@ -3518,16 +3543,18 @@ __metadata: linkType: hard "typescript-eslint@npm:^8.8.1": - version: 8.11.0 - resolution: "typescript-eslint@npm:8.11.0" + version: 8.15.0 + resolution: "typescript-eslint@npm:8.15.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.11.0" - "@typescript-eslint/parser": "npm:8.11.0" - "@typescript-eslint/utils": "npm:8.11.0" + "@typescript-eslint/eslint-plugin": "npm:8.15.0" + "@typescript-eslint/parser": "npm:8.15.0" + "@typescript-eslint/utils": "npm:8.15.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/8f9b5916c9f47b0cbb26f142d1a266a6aaf33998ec87621252dffb56d8fe0ad01a944f8d8d837e4e6058153a1deee3557527d14fa7bf7ef80a927334529db6bd + checksum: 10c0/589aebf0d0b9b79db1cd0b7c2ea08c6b5727c1db095d39077d070c332066c7d549a0eb2ef60b0d41619720c317c1955236c5c8ee6320bc7c6ae475add7223b55 languageName: node linkType: hard @@ -3563,7 +3590,7 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.2": +"undici-types@npm:~6.19.8": version: 6.19.8 resolution: "undici-types@npm:6.19.8" checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 @@ -3622,16 +3649,16 @@ __metadata: linkType: hard "viem@npm:^2.21.25": - version: 2.21.32 - resolution: "viem@npm:2.21.32" + version: 2.21.48 + resolution: "viem@npm:2.21.48" dependencies: - "@adraffy/ens-normalize": "npm:1.11.0" "@noble/curves": "npm:1.6.0" "@noble/hashes": "npm:1.5.0" "@scure/bip32": "npm:1.5.0" "@scure/bip39": "npm:1.4.0" abitype: "npm:1.0.6" isows: "npm:1.0.6" + ox: "npm:0.1.2" webauthn-p256: "npm:0.0.10" ws: "npm:8.18.0" peerDependencies: @@ -3639,7 +3666,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/1a38f1fbb9e71eafd5d923d737f6869a700b32d12e5e735f9b7cea4692971ee869bb8f7e1a265ba80b81ba2e0e129137d868d33b2faba260d3a06915d75306a5 + checksum: 10c0/e9b2799535263a859bddda25d962b13d2c76aec191e1849dd0f268c32a43eb65932a05cc5be270c92e19d79aafda73884690c0b0fbdb9311266a01ea3f659082 languageName: node linkType: hard From 006ef2607be7c11996297d996bf676ef91d8723f Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:35:38 +0100 Subject: [PATCH 43/53] fix tests --- src/tests/Vault.test.ts | 34 +++++++++++++++++----------------- src/utils/BigInt.ts | 3 +++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/tests/Vault.test.ts b/src/tests/Vault.test.ts index 6ed87f4..dc28ad9 100644 --- a/src/tests/Vault.test.ts +++ b/src/tests/Vault.test.ts @@ -15,13 +15,13 @@ const vaultAddress = '0x05eb35c2e4fa21fb06d3fab92916191b254b3504' // Active investor with a pending redeem order const investorA = '0x423420Ae467df6e90291fd0252c0A8a637C1e03f' // Permissioned investor with no orders -const investorB = '0xa076b817Fade13Ee72C495910eDCe1ed953F9930' // E2E Admin +const investorB = '0xa076b817Fade13Ee72C495910eDCe1ed953F9930' // Investor with a claimable invest order -const investorC = '0x7fAbAa12da2E30650c841AC647e3567f942fcdf5' // E2E Borrower +const investorC = '0x7fAbAa12da2E30650c841AC647e3567f942fcdf5' // Non-permissioned investor -const investorD = '0x63892115da2e40f8135Abe99Dc5155dd552464F4' // E2E Nav Manager +const investorD = '0x63892115da2e40f8135Abe99Dc5155dd552464F4' // Investor with a claimable cancel deposit -const investorE = '0x655631E9F3d31a70DD6c9B4cFB5CfDe7445Fd0d2' // E2E Fee receiver +const investorE = '0x655631E9F3d31a70DD6c9B4cFB5CfDe7445Fd0d2' describe('Vault', () => { let vault: Vault @@ -73,13 +73,13 @@ describe('Vault', () => { context.centrifuge.setSigner(context.tenderlyFork.signer) const [result, investmentAfter] = await Promise.all([ lastValueFrom(vault.placeInvestOrder(100000000n).pipe(toArray())), - firstValueFrom(vault.investment(investorB).pipe(skipWhile((i) => i.pendingInvestCurrency !== 100000000n))), + firstValueFrom(vault.investment(investorB).pipe(skipWhile((i) => !i.pendingInvestCurrency.eq(100000000n)))), ]) expect(result[2]?.type).to.equal('TransactionConfirmed') expect((result[2] as any).title).to.equal('Approve') expect(result[5]?.type).to.equal('TransactionConfirmed') expect((result[5] as any).title).to.equal('Invest') - expect(investmentAfter.pendingInvestCurrency).to.equal(100000000n) + expect(investmentAfter.pendingInvestCurrency.toBigInt()).to.equal(100000000n) }) it('should cancel an invest order', async () => { @@ -97,15 +97,15 @@ describe('Vault', () => { it('should claim a processed cancellation', async () => { const investmentBefore = await vault.investment(investorE) - expect(investmentBefore.claimableCancelInvestCurrency).to.equal(1234000000n) + expect(investmentBefore.claimableCancelInvestCurrency.toBigInt()).to.equal(1234000000n) context.tenderlyFork.impersonateAddress = investorE context.centrifuge.setSigner(context.tenderlyFork.signer) const [result, investmentAfter] = await Promise.all([ vault.claim(), - firstValueFrom(vault.investment(investorE).pipe(skipWhile((i) => i.claimableCancelInvestCurrency !== 0n))), + firstValueFrom(vault.investment(investorE).pipe(skipWhile((i) => !i.claimableCancelInvestCurrency.isZero()))), ]) expect(result.type).to.equal('TransactionConfirmed') - expect(investmentAfter.claimableCancelInvestCurrency).to.equal(0n) + expect(investmentAfter.claimableCancelInvestCurrency.isZero()).to.equal(true) }) it('should throw when trying to cancel a non-existing order', async () => { @@ -124,18 +124,18 @@ describe('Vault', () => { it('should claim an executed order', async () => { const investmentBefore = await vault.investment(investorC) - expect(investmentBefore.claimableInvestShares).to.equal(939254224n) - expect(investmentBefore.claimableInvestCurrencyEquivalent).to.equal(999999999n) + expect(investmentBefore.claimableInvestShares.toBigInt()).to.equal(939254224n) + expect(investmentBefore.claimableInvestCurrencyEquivalent.toBigInt()).to.equal(999999999n) context.tenderlyFork.impersonateAddress = investorC context.centrifuge.setSigner(context.tenderlyFork.signer) const [result, investmentAfter] = await Promise.all([ vault.claim(), - firstValueFrom(vault.investment(investorC).pipe(skipWhile((i) => i.claimableInvestShares !== 0n))), + firstValueFrom(vault.investment(investorC).pipe(skipWhile((i) => !i.claimableInvestShares.isZero()))), ]) expect(result.type).to.equal('TransactionConfirmed') - expect(investmentAfter.claimableInvestShares).to.equal(0n) - expect(investmentAfter.claimableInvestCurrencyEquivalent).to.equal(0n) - expect(investmentAfter.shareBalance).to.equal(939254224n) + expect(investmentAfter.claimableInvestShares.isZero()).to.equal(true) + expect(investmentAfter.claimableInvestCurrencyEquivalent.isZero()).to.equal(true) + expect(investmentAfter.shareBalance.toBigInt()).to.equal(939254224n) }) it('should place a redeem order', async () => { @@ -143,11 +143,11 @@ describe('Vault', () => { context.centrifuge.setSigner(context.tenderlyFork.signer) const [result, investmentAfter] = await Promise.all([ lastValueFrom(vault.placeRedeemOrder(939254224n).pipe(toArray())), - firstValueFrom(vault.investment(investorC).pipe(skipWhile((i) => i.pendingRedeemShares !== 939254224n))), + firstValueFrom(vault.investment(investorC).pipe(skipWhile((i) => !i.pendingRedeemShares.eq(939254224n)))), ]) expect(result[2]?.type).to.equal('TransactionConfirmed') expect((result[2] as any).title).to.equal('Redeem') - expect(investmentAfter.pendingRedeemShares).to.equal(939254224n) + expect(investmentAfter.pendingRedeemShares.toBigInt()).to.equal(939254224n) }) it('should cancel a redeem order', async () => { diff --git a/src/utils/BigInt.ts b/src/utils/BigInt.ts index 371ef33..3f3640b 100644 --- a/src/utils/BigInt.ts +++ b/src/utils/BigInt.ts @@ -122,6 +122,9 @@ export class DecimalWrapper extends BigIntWrapper { const val = typeof value === 'bigint' ? value : value.toBigInt() return this.value === val } + isZero() { + return this.value === 0n + } } export class Currency extends DecimalWrapper { From 2b033a6d891696980db9b07b17fdf54e2402b872 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:44:05 +0100 Subject: [PATCH 44/53] more tests --- src/Pool.test.ts | 20 +++++++++++++++++++ src/Pool.ts | 4 ++++ src/PoolNetwork.test.ts | 37 +++++++++++++++++++++++++++++++++++ src/PoolNetwork.ts | 1 - src/{tests => }/Vault.test.ts | 0 src/abi/PoolManager.abi.json | 2 +- src/tests/setup.ts | 10 +++++++--- 7 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/Pool.test.ts create mode 100644 src/PoolNetwork.test.ts rename src/{tests => }/Vault.test.ts (100%) diff --git a/src/Pool.test.ts b/src/Pool.test.ts new file mode 100644 index 0000000..1d6932a --- /dev/null +++ b/src/Pool.test.ts @@ -0,0 +1,20 @@ +import { expect } from 'chai' +import { Pool } from './Pool.js' +import { context } from './tests/setup.js' + +const poolId = '2779829532' + +describe('Pool', () => { + let pool: Pool + beforeEach(() => { + const { centrifuge } = context + pool = new Pool(centrifuge, poolId) + }) + + it('get active networks of a pool', async () => { + const networks = await pool.activeNetworks() + expect(networks).to.have.length(2) + expect(networks[0]!.chainId).to.equal(11155111) + expect(networks[1]!.chainId).to.equal(84532) + }) +}) diff --git a/src/Pool.ts b/src/Pool.ts index 11eb865..2be3196 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -85,4 +85,8 @@ export class Pool extends Entity { ) }) } + + vault(chainId: number, trancheId: string, asset: string) { + return this._query(null, () => this.network(chainId).pipe(switchMap((network) => network.vault(trancheId, asset)))) + } } diff --git a/src/PoolNetwork.test.ts b/src/PoolNetwork.test.ts new file mode 100644 index 0000000..7ab8ea1 --- /dev/null +++ b/src/PoolNetwork.test.ts @@ -0,0 +1,37 @@ +import { expect } from 'chai' +import sinon from 'sinon' +import { Pool } from './Pool.js' +import { PoolNetwork } from './PoolNetwork.js' +import { context } from './tests/setup.js' + +const poolId = '2779829532' +const trancheId = '0xac6bffc5fd68f7772ceddec7b0a316ca' +const vaultAddress = '0x05eb35c2e4fa21fb06d3fab92916191b254b3504' + +describe('PoolNetwork', () => { + let poolNetwork: PoolNetwork + beforeEach(() => { + const { centrifuge } = context + const pool = new Pool(centrifuge, poolId) + poolNetwork = new PoolNetwork(centrifuge, pool, 11155111) + }) + + it('should get whether a pool is deployed to a network', async () => { + const isActive = await poolNetwork.isActive() + expect(isActive).to.equal(true) + + // non-active pool/network + const poolNetwork2 = new PoolNetwork(context.centrifuge, new Pool(context.centrifuge, '123'), 11155111) + const isActive2 = await poolNetwork2.isActive() + expect(isActive2).to.equal(false) + }) + + it('get vaults for a tranche', async () => { + const fetchSpy = sinon.spy(globalThis, 'fetch') + const vaults = await poolNetwork.vaults(trancheId) + expect(vaults).to.have.length(1) + expect(vaults[0]!.address.toLowerCase()).to.equal(vaultAddress) + // Calls should get batched + expect(fetchSpy.getCalls().length).to.equal(1) + }) +}) diff --git a/src/PoolNetwork.ts b/src/PoolNetwork.ts index 975a31a..e2e98a0 100644 --- a/src/PoolNetwork.ts +++ b/src/PoolNetwork.ts @@ -101,7 +101,6 @@ export class PoolNetwork extends Entity { return new Vault(this._root, this, trancheId, curAddr, vaultAddr) }) ) - console.log('results', results) return results.filter((result) => result.status === 'fulfilled').map((result) => result.value) }).pipe( repeatOnEvents( diff --git a/src/tests/Vault.test.ts b/src/Vault.test.ts similarity index 100% rename from src/tests/Vault.test.ts rename to src/Vault.test.ts diff --git a/src/abi/PoolManager.abi.json b/src/abi/PoolManager.abi.json index 1243bf8..5d5b489 100644 --- a/src/abi/PoolManager.abi.json +++ b/src/abi/PoolManager.abi.json @@ -30,7 +30,7 @@ "function gateway() view returns (address)", "function getTranche(uint64 poolId, bytes16 trancheId) view returns (address)", "function getTranchePrice(uint64 poolId, bytes16 trancheId, address asset) view returns (uint128 price, uint64 computedAt)", - "function getVault(uint64 poolId, bytes16 trancheId, uint128 assetId) view returns (address)", + "function getVault_(uint64 poolId, bytes16 trancheId, uint128 assetId) view returns (address)", "function getVault(uint64 poolId, bytes16 trancheId, address asset) view returns (address)", "function getVaultAsset(address vault) view returns (address, bool)", "function handle(bytes message)", diff --git a/src/tests/setup.ts b/src/tests/setup.ts index fe7fbd8..a487c2c 100644 --- a/src/tests/setup.ts +++ b/src/tests/setup.ts @@ -3,18 +3,22 @@ import { Centrifuge } from '../Centrifuge.js' import { TenderlyFork } from './tenderly.js' class TestContext { - public centrifuge!: Centrifuge + #centrifuge: Centrifuge | null = null public tenderlyFork!: TenderlyFork + get centrifuge() { + return this.#centrifuge ?? (this.#centrifuge = new Centrifuge({ environment: 'demo' })) + } + async initialize() { this.tenderlyFork = await TenderlyFork.create(sepolia) - this.centrifuge = new Centrifuge({ + this.#centrifuge = new Centrifuge({ environment: 'demo', rpcUrls: { 11155111: this.tenderlyFork.rpcUrl, }, }) - this.centrifuge.setSigner(this.tenderlyFork.signer) + this.#centrifuge.setSigner(this.tenderlyFork.signer) } async cleanup() { From 333f3c4089778c39ed6978c7c248bdf4829f6376 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:44:59 +0100 Subject: [PATCH 45/53] fix path --- src/Vault.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Vault.test.ts b/src/Vault.test.ts index dc28ad9..22594b6 100644 --- a/src/Vault.test.ts +++ b/src/Vault.test.ts @@ -1,11 +1,11 @@ import { expect } from 'chai' import { firstValueFrom, lastValueFrom, skip, skipWhile, tap, toArray } from 'rxjs' import sinon from 'sinon' -import { ABI } from '../abi/index.js' -import { Pool } from '../Pool.js' -import { PoolNetwork } from '../PoolNetwork.js' -import { Vault } from '../Vault.js' -import { context } from './setup.js' +import { ABI } from './abi/index.js' +import { Pool } from './Pool.js' +import { PoolNetwork } from './PoolNetwork.js' +import { context } from './tests/setup.js' +import { Vault } from './Vault.js' const poolId = '2779829532' const trancheId = '0xac6bffc5fd68f7772ceddec7b0a316ca' From 06d2c1db8994f00670b4a8c6bd9d53e08fc69a4a Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:00:19 +0100 Subject: [PATCH 46/53] fix cache expiry --- src/Centrifuge.ts | 9 ++++++++- src/tests/Centrifuge.test.ts | 37 ++++++++++++++++++++++++++++++++-- src/utils/rx.ts | 39 +++++++++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index e41f634..cfe07db 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -1,5 +1,6 @@ import type { Observable } from 'rxjs' import { + catchError, concatWith, defaultIfEmpty, defer, @@ -40,7 +41,7 @@ import type { CentrifugeQueryOptions, Query } from './types/query.js' import type { OperationStatus, Signer, Transaction, TransactionCallbackParams } from './types/transaction.js' import { Currency } from './utils/BigInt.js' import { hashKey } from './utils/query.js' -import { makeThenable, repeatOnEvents, shareReplayWithDelayedReset } from './utils/rx.js' +import { ExpiredCacheError, makeThenable, repeatOnEvents, shareReplayWithDelayedReset } from './utils/rx.js' import { doTransaction, isLocalAccount } from './utils/transaction.js' export type Config = { @@ -418,6 +419,12 @@ export class Centrifuge { // For new subscribers, recreate the shared observable if the previously shared observable has completed // and no longer has a cached value, which can happen with a finite `valueCacheTime`. defaultIfEmpty(defer(createShared)), + // For new subscribers, recreate the shared observable if the previously cached value is expired, + // without the observable having completed. + catchError((err) => { + if (err instanceof ExpiredCacheError) defer(createShared) + throw err + }), // For existing subscribers, merge any newly created shared observable. concatWith(sharedSubject), mergeMap((d) => (isObservable(d) ? d : of(d))) diff --git a/src/tests/Centrifuge.test.ts b/src/tests/Centrifuge.test.ts index 2ddba9a..81e05c6 100644 --- a/src/tests/Centrifuge.test.ts +++ b/src/tests/Centrifuge.test.ts @@ -180,6 +180,39 @@ describe('Centrifuge', () => { expect(value2).to.equal(3) expect(value3).to.equal(6) }) + + it('should update dependant queries with values from dependencies', async () => { + let i = 0 + const query1 = context.centrifuge._query(['key3'], () => of(++i), { valueCacheTime: 120 }) + const query2 = context.centrifuge._query(['key4'], () => query1.pipe(map((v1) => v1 * 10))) + const value1 = await query2 + clock.tick(150_000) + const value3 = await query2 + expect(value1).to.equal(10) + expect(value3).to.equal(20) + }) + + it('should recreate the shared observable when the cached value is expired', async () => { + let i = 0 + const query1 = context.centrifuge._query( + null, + () => + defer(async function* () { + yield await lazy(`${++i}-A`) + yield await lazy(`${i}-B`, 5000) + }), + { valueCacheTime: 1 } + ) + let lastValue = '' + const subscription1 = query1.subscribe((next) => (lastValue = next)) + const value1 = await query1 + clock.tick(2_000) + const value2 = await query1 + expect(value1).to.equal('1-A') + expect(value2).to.equal('2-A') + expect(lastValue).to.equal('2-A') + subscription1.unsubscribe() + }) }) describe('Transactions', () => { @@ -285,8 +318,8 @@ describe('Centrifuge', () => { }) }) -function lazy(value: T) { - return new Promise((res) => setTimeout(() => res(value), 10)) +function lazy(value: T, t = 10) { + return new Promise((res) => setTimeout(() => res(value), t)) } function mockProvider({ chainId = 11155111, accounts = ['0x2'] } = {}) { diff --git a/src/utils/rx.ts b/src/utils/rx.ts index d23f5ed..02ab555 100644 --- a/src/utils/rx.ts +++ b/src/utils/rx.ts @@ -1,8 +1,9 @@ -import type { MonoTypeOperatorFunction, Observable } from 'rxjs' -import { filter, firstValueFrom, lastValueFrom, repeat, ReplaySubject, share, Subject, timer } from 'rxjs' +import type { MonoTypeOperatorFunction, Observable, Subscriber, Subscription } from 'rxjs' +import { filter, firstValueFrom, lastValueFrom, repeat, ReplaySubject, Subject, timer } from 'rxjs' import type { Abi, Log } from 'viem' import type { Centrifuge } from '../Centrifuge.js' import type { Query } from '../types/query.js' +import { share } from './share.js' export function shareReplayWithDelayedReset(config?: { bufferSize?: number @@ -12,7 +13,7 @@ export function shareReplayWithDelayedReset(config?: { const { bufferSize = Infinity, windowTime = Infinity, resetDelay = 1000 } = config ?? {} const reset = resetDelay === 0 ? true : isFinite(resetDelay) ? () => timer(resetDelay) : false return share({ - connector: () => (bufferSize === 0 ? new Subject() : new ReplaySubject(bufferSize, windowTime)), + connector: () => (bufferSize === 0 ? new Subject() : new ExpiringReplaySubject(bufferSize, windowTime)), resetOnError: true, resetOnComplete: false, resetOnRefCountZero: reset, @@ -50,3 +51,35 @@ export function makeThenable($query: Observable, exhaust = false) { }) return thenableQuery } + +export class ExpiredCacheError extends Error {} + +class ExpiringReplaySubject extends ReplaySubject { + // Re-implementation of ReplaySubject._subscribe that throws when an existing buffer is expired + // @ts-expect-error + protected override _subscribe(subscriber: Subscriber): Subscription { + // @ts-expect-error + const length = this._buffer.length + // @ts-expect-error + this._throwIfClosed() + // @ts-expect-error + this._trimBuffer() + // @ts-expect-error + const { _infiniteTimeWindow, _buffer } = this + const copy = _buffer.slice() + for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) { + subscriber.next(copy[i] as T) + } + // @ts-expect-error + const { _buffer: bufferAfter } = this + if (length && bufferAfter.length === 0) { + this.complete() + throw new ExpiredCacheError() + } + // @ts-expect-error + const subscription = this._innerSubscribe(subscriber) + // @ts-expect-error + this._checkFinalizedStatuses(subscriber) + return subscription + } +} From 44493ccd61693849828d01e92d9efec4dd8821ae Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:32:41 +0100 Subject: [PATCH 47/53] fix share --- src/utils/rx.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/utils/rx.ts b/src/utils/rx.ts index 02ab555..f4eccfb 100644 --- a/src/utils/rx.ts +++ b/src/utils/rx.ts @@ -1,9 +1,8 @@ import type { MonoTypeOperatorFunction, Observable, Subscriber, Subscription } from 'rxjs' -import { filter, firstValueFrom, lastValueFrom, repeat, ReplaySubject, Subject, timer } from 'rxjs' +import { filter, firstValueFrom, lastValueFrom, repeat, ReplaySubject, share, Subject, timer } from 'rxjs' import type { Abi, Log } from 'viem' import type { Centrifuge } from '../Centrifuge.js' import type { Query } from '../types/query.js' -import { share } from './share.js' export function shareReplayWithDelayedReset(config?: { bufferSize?: number @@ -59,20 +58,17 @@ class ExpiringReplaySubject extends ReplaySubject { // @ts-expect-error protected override _subscribe(subscriber: Subscriber): Subscription { // @ts-expect-error - const length = this._buffer.length + const { _infiniteTimeWindow, _buffer } = this + const length = _buffer.length // @ts-expect-error this._throwIfClosed() // @ts-expect-error this._trimBuffer() - // @ts-expect-error - const { _infiniteTimeWindow, _buffer } = this const copy = _buffer.slice() for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) { subscriber.next(copy[i] as T) } - // @ts-expect-error - const { _buffer: bufferAfter } = this - if (length && bufferAfter.length === 0) { + if (length && _buffer.length === 0) { this.complete() throw new ExpiredCacheError() } From 03f95d156dafd37686ed482a52f832398adb8e84 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:02:36 +0100 Subject: [PATCH 48/53] simplify and fix tests --- src/Centrifuge.ts | 16 +++++----------- src/utils/rx.ts | 20 ++++---------------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index cfe07db..8fd3c9d 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -1,6 +1,5 @@ import type { Observable } from 'rxjs' import { - catchError, concatWith, defaultIfEmpty, defer, @@ -41,7 +40,7 @@ import type { CentrifugeQueryOptions, Query } from './types/query.js' import type { OperationStatus, Signer, Transaction, TransactionCallbackParams } from './types/transaction.js' import { Currency } from './utils/BigInt.js' import { hashKey } from './utils/query.js' -import { ExpiredCacheError, makeThenable, repeatOnEvents, shareReplayWithDelayedReset } from './utils/rx.js' +import { makeThenable, repeatOnEvents, shareReplayWithDelayedReset } from './utils/rx.js' import { doTransaction, isLocalAccount } from './utils/transaction.js' export type Config = { @@ -403,7 +402,8 @@ export class Centrifuge { _query(keys: any[] | null, observableCallback: () => Observable, options?: CentrifugeQueryOptions): Query { function get() { const sharedSubject = new Subject>() - function createShared(): Observable { + function createShared(a?: string): Observable { + if (a) console.log('createShared', a) const $shared = observableCallback().pipe( shareReplayWithDelayedReset({ bufferSize: (options?.cache ?? true) ? 1 : 0, @@ -417,14 +417,8 @@ export class Centrifuge { const $query = createShared().pipe( // For new subscribers, recreate the shared observable if the previously shared observable has completed - // and no longer has a cached value, which can happen with a finite `valueCacheTime`. - defaultIfEmpty(defer(createShared)), - // For new subscribers, recreate the shared observable if the previously cached value is expired, - // without the observable having completed. - catchError((err) => { - if (err instanceof ExpiredCacheError) defer(createShared) - throw err - }), + // and no longer has a cached value, or if the previously cached value is expired without the observable having completed. + defaultIfEmpty(defer(() => createShared('EMPTY'))), // For existing subscribers, merge any newly created shared observable. concatWith(sharedSubject), mergeMap((d) => (isObservable(d) ? d : of(d))) diff --git a/src/utils/rx.ts b/src/utils/rx.ts index f4eccfb..fe5b98b 100644 --- a/src/utils/rx.ts +++ b/src/utils/rx.ts @@ -51,31 +51,19 @@ export function makeThenable($query: Observable, exhaust = false) { return thenableQuery } -export class ExpiredCacheError extends Error {} - class ExpiringReplaySubject extends ReplaySubject { - // Re-implementation of ReplaySubject._subscribe that throws when an existing buffer is expired + // Re-implementation of ReplaySubject._subscribe that completes the subject when an existing buffer is expired // @ts-expect-error protected override _subscribe(subscriber: Subscriber): Subscription { // @ts-expect-error - const { _infiniteTimeWindow, _buffer } = this + const { _buffer } = this const length = _buffer.length // @ts-expect-error - this._throwIfClosed() - // @ts-expect-error - this._trimBuffer() - const copy = _buffer.slice() - for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) { - subscriber.next(copy[i] as T) - } + const subscription = super._subscribe(subscriber) + if (length && _buffer.length === 0) { this.complete() - throw new ExpiredCacheError() } - // @ts-expect-error - const subscription = this._innerSubscribe(subscriber) - // @ts-expect-error - this._checkFinalizedStatuses(subscriber) return subscription } } From 98a8491a6b2eb0eaceadfb4c965584133e0537a9 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:04:34 +0100 Subject: [PATCH 49/53] remove log --- src/Centrifuge.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 8fd3c9d..1ed0cd0 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -402,8 +402,7 @@ export class Centrifuge { _query(keys: any[] | null, observableCallback: () => Observable, options?: CentrifugeQueryOptions): Query { function get() { const sharedSubject = new Subject>() - function createShared(a?: string): Observable { - if (a) console.log('createShared', a) + function createShared(): Observable { const $shared = observableCallback().pipe( shareReplayWithDelayedReset({ bufferSize: (options?.cache ?? true) ? 1 : 0, @@ -418,7 +417,7 @@ export class Centrifuge { const $query = createShared().pipe( // For new subscribers, recreate the shared observable if the previously shared observable has completed // and no longer has a cached value, or if the previously cached value is expired without the observable having completed. - defaultIfEmpty(defer(() => createShared('EMPTY'))), + defaultIfEmpty(defer(createShared)), // For existing subscribers, merge any newly created shared observable. concatWith(sharedSubject), mergeMap((d) => (isObservable(d) ? d : of(d))) From 79b85720d2217c6f2d011328518d4477f6fad624 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:32:31 +0100 Subject: [PATCH 50/53] comments --- src/Centrifuge.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index 1ed0cd0..c4599f8 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -415,13 +415,16 @@ export class Centrifuge { } const $query = createShared().pipe( - // For new subscribers, recreate the shared observable if the previously shared observable has completed - // and no longer has a cached value, or if the previously cached value is expired without the observable having completed. + // When `valueCacheTime` is finite, and the cached value is expired, + // the shared observable will immediately complete upon the next subscription. + // This will cause the shared observable to be recreated. defaultIfEmpty(defer(createShared)), // For existing subscribers, merge any newly created shared observable. concatWith(sharedSubject), + // Flatten observables emitted from the `sharedSubject` mergeMap((d) => (isObservable(d) ? d : of(d))) ) + return makeThenable($query) } return keys ? this.#memoizeWith(keys, get) : get() From 2e5ea2d3536f9ffb132125ac3f5333279fdb5225 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:59:31 +0100 Subject: [PATCH 51/53] fix tests --- src/Centrifuge.ts | 7 +- src/PoolNetwork.ts | 156 +++++++++++++++++++++++++++++++++++++- src/Vault.test.ts | 24 +++--- src/Vault.ts | 183 ++++++++++++++++++--------------------------- src/utils/rx.ts | 2 +- 5 files changed, 240 insertions(+), 132 deletions(-) diff --git a/src/Centrifuge.ts b/src/Centrifuge.ts index c4599f8..0346ba9 100644 --- a/src/Centrifuge.ts +++ b/src/Centrifuge.ts @@ -4,7 +4,6 @@ import { defaultIfEmpty, defer, filter, - firstValueFrom, identity, isObservable, map, @@ -467,14 +466,12 @@ export class Centrifuge { */ _transact( title: string, - transactionCallback: (params: TransactionCallbackParams) => Promise | Observable, + transactionCallback: (params: TransactionCallbackParams) => Promise, chainId?: number ) { return this._transactSequence(async function* (params) { const transaction = transactionCallback(params) - yield* doTransaction(title, params.publicClient, () => - 'then' in transaction ? transaction : firstValueFrom(transaction) - ) + yield* doTransaction(title, params.publicClient, () => transaction) }, chainId) } diff --git a/src/PoolNetwork.ts b/src/PoolNetwork.ts index e2e98a0..b68def7 100644 --- a/src/PoolNetwork.ts +++ b/src/PoolNetwork.ts @@ -1,5 +1,5 @@ import { combineLatest, defer, map, switchMap } from 'rxjs' -import { getContract } from 'viem' +import { getContract, toHex } from 'viem' import { ABI } from './abi/index.js' import type { Centrifuge } from './Centrifuge.js' import { lpConfig } from './config/lp.js' @@ -7,6 +7,7 @@ import { Entity } from './Entity.js' import type { Pool } from './Pool.js' import type { HexString } from './types/index.js' import { repeatOnEvents } from './utils/rx.js' +import { doTransaction } from './utils/transaction.js' import { Vault } from './Vault.js' /** @@ -78,6 +79,72 @@ export class PoolNetwork extends Entity { ) } + /** + * Estimates the gas cost needed to bridge the message that results from a transaction. + * @internal + */ + _estimate() { + return this._root._query(['estimate', this.chainId], () => + defer(() => { + const bytes = toHex(new Uint8Array([0x12])) + const { centrifugeRouter } = lpConfig[this.chainId]! + return this._root.getClient(this.chainId)!.readContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName: 'estimate', + args: [bytes], + }) as Promise + }) + ) + } + + /** + * Get the contract address of the share token. + * @internal + */ + _share(trancheId: string) { + return this._query(['share'], () => + this._poolManager().pipe( + switchMap((poolManager) => + defer( + () => + this._root.getClient(this.chainId)!.readContract({ + address: poolManager, + abi: ABI.PoolManager, + functionName: 'getTranche', + args: [this.pool.id, trancheId], + }) as Promise + ).pipe( + repeatOnEvents( + this._root, + { + address: poolManager, + abi: ABI.PoolManager, + eventName: 'DeployTranche', + filter: (events) => { + return events.some( + (event) => String(event.args.poolId) === this.pool.id && event.args.trancheId === trancheId + ) + }, + }, + this.chainId + ) + ) + ) + ) + ) + } + + /** + * Get the details of the share token. + * @param trancheId - The tranche ID + */ + shareCurrency(trancheId: string) { + return this._query(null, () => + this._share(trancheId).pipe(switchMap((share) => this._root.currency(share, this.chainId))) + ) + } + /** * Get the deployed Vaults for a given tranche. There may exist one Vault for each allowed investment currency. * Vaults are used to submit/claim investments and redemptions. @@ -110,9 +177,9 @@ export class PoolNetwork extends Entity { abi: ABI.PoolManager, eventName: 'DeployVault', filter: (events) => { - return events.some((event) => { - return String(event.args.poolId) === this.pool.id || event.args.trancheId === trancheId - }) + return events.some( + (event) => String(event.args.poolId) === this.pool.id && event.args.trancheId === trancheId + ) }, }, this.chainId @@ -191,4 +258,85 @@ export class PoolNetwork extends Entity { ) ) } + + /** + * Get whether a pool is active and the tranche token can be deployed. + * @param trancheId - The tranche ID + */ + canTrancheBeDeployed(trancheId: string) { + return this._query(['canTrancheBeDeployed'], () => + this._poolManager().pipe( + switchMap((manager) => { + return defer( + () => + this._root.getClient(this.chainId)!.readContract({ + address: manager, + abi: ABI.PoolManager, + functionName: 'canTrancheBeDeployed', + args: [this.pool.id, trancheId], + }) as Promise + ).pipe( + repeatOnEvents( + this._root, + { + address: manager, + abi: ABI.PoolManager, + eventName: 'DeployTranche', + filter: (events) => { + return events.some( + (event) => String(event.args.poolId) === this.pool.id && event.args.trancheId === trancheId + ) + }, + }, + this.chainId + ) + ) + }) + ) + ) + } + + /** + * Deploy a tranche token for the pool. + * @param trancheId - The tranche ID + */ + deployTranche(trancheId: string) { + const self = this + return this._transactSequence(async function* ({ walletClient, publicClient }) { + const [poolManager, canTrancheBeDeployed] = await Promise.all([ + self._poolManager(), + self.canTrancheBeDeployed(trancheId), + ]) + if (!canTrancheBeDeployed) throw new Error('Pool is not active on this network') + yield* doTransaction('Deploy Tranche', publicClient, () => + walletClient.writeContract({ + address: poolManager, + abi: ABI.PoolManager, + functionName: 'deployTranche', + args: [self.pool.id, trancheId], + }) + ) + }, this.chainId) + } + + /** + * Deploy a vault for a specific tranche x currency combination. + * @param trancheId - The tranche ID + * @param currencyAddress - The investment currency address + */ + deployVault(trancheId: string, currencyAddress: string) { + const self = this + return this._transactSequence(async function* ({ walletClient, publicClient }) { + const [poolManager, trancheToken] = await Promise.all([self._poolManager(), self._share(trancheId)]) + if (!trancheToken) throw new Error('Pool is not active on this network') + yield* doTransaction('Deploy Vault', publicClient, () => + walletClient.writeContract({ + address: poolManager, + abi: ABI.PoolManager, + functionName: 'deployVault', + args: [self.pool.id, trancheId, currencyAddress], + }) + ) + }, this.chainId) + } } diff --git a/src/Vault.test.ts b/src/Vault.test.ts index 22594b6..2f35164 100644 --- a/src/Vault.test.ts +++ b/src/Vault.test.ts @@ -37,42 +37,42 @@ describe('Vault', () => { const investment = await vault.investment(investorA) expect(investment.isAllowedToInvest).to.equal(true) // Calls should get batched - expect(fetchSpy.getCalls().length).to.equal(4) + expect(fetchSpy.getCalls().length).to.equal(6) }) it("should throw when placing an invest order larger than the users's balance", async () => { context.tenderlyFork.impersonateAddress = investorB context.centrifuge.setSigner(context.tenderlyFork.signer) let error: Error | null = null - let wasAskedToSign = false + let emittedSigningStatus = false try { - await lastValueFrom(vault.placeInvestOrder(1000000000000000n).pipe(tap(() => (wasAskedToSign = true)))) + await lastValueFrom(vault.increaseInvestOrder(1000000000000000n).pipe(tap(() => (emittedSigningStatus = true)))) } catch (e: any) { error = e } expect(error?.message).to.equal('Insufficient balance') - expect(wasAskedToSign).to.equal(false) + expect(emittedSigningStatus).to.equal(false) }) it('should throw when not allowed to invest', async () => { context.tenderlyFork.impersonateAddress = investorD context.centrifuge.setSigner(context.tenderlyFork.signer) let error: Error | null = null - let wasAskedToSign = false + let emittedSigningStatus = false try { - await lastValueFrom(vault.placeInvestOrder(100000000n).pipe(tap(() => (wasAskedToSign = true)))) + await lastValueFrom(vault.increaseInvestOrder(100000000n).pipe(tap(() => (emittedSigningStatus = true)))) } catch (e: any) { error = e } expect(error?.message).to.equal('Not allowed to invest') - expect(wasAskedToSign).to.equal(false) + expect(emittedSigningStatus).to.equal(false) }) it('should place an invest order', async () => { context.tenderlyFork.impersonateAddress = investorB context.centrifuge.setSigner(context.tenderlyFork.signer) const [result, investmentAfter] = await Promise.all([ - lastValueFrom(vault.placeInvestOrder(100000000n).pipe(toArray())), + lastValueFrom(vault.increaseInvestOrder(100000000n).pipe(toArray())), firstValueFrom(vault.investment(investorB).pipe(skipWhile((i) => !i.pendingInvestCurrency.eq(100000000n)))), ]) expect(result[2]?.type).to.equal('TransactionConfirmed') @@ -112,14 +112,14 @@ describe('Vault', () => { context.tenderlyFork.impersonateAddress = investorB context.centrifuge.setSigner(context.tenderlyFork.signer) let thrown = false - let wasAskedToSign = false + let emittedSigningStatus = false try { - await lastValueFrom(vault.cancelRedeemOrder().pipe(tap(() => (wasAskedToSign = true)))) + await lastValueFrom(vault.cancelRedeemOrder().pipe(tap(() => (emittedSigningStatus = true)))) } catch { thrown = true } expect(thrown).to.equal(true) - expect(wasAskedToSign).to.equal(false) + expect(emittedSigningStatus).to.equal(false) }) it('should claim an executed order', async () => { @@ -142,7 +142,7 @@ describe('Vault', () => { context.tenderlyFork.impersonateAddress = investorC context.centrifuge.setSigner(context.tenderlyFork.signer) const [result, investmentAfter] = await Promise.all([ - lastValueFrom(vault.placeRedeemOrder(939254224n).pipe(toArray())), + lastValueFrom(vault.increaseRedeemOrder(939254224n).pipe(toArray())), firstValueFrom(vault.investment(investorC).pipe(skipWhile((i) => !i.pendingRedeemShares.eq(939254224n)))), ]) expect(result[2]?.type).to.equal('TransactionConfirmed') diff --git a/src/Vault.ts b/src/Vault.ts index 401c7f1..68d6fad 100644 --- a/src/Vault.ts +++ b/src/Vault.ts @@ -1,5 +1,5 @@ import { combineLatest, defer, map, switchMap } from 'rxjs' -import { encodeFunctionData, getContract, toHex } from 'viem' +import { encodeFunctionData, getContract } from 'viem' import type { Centrifuge } from './Centrifuge.js' import { Entity } from './Entity.js' import type { Pool } from './Pool.js' @@ -41,58 +41,20 @@ export class Vault extends Entity { this.address = address.toLowerCase() as HexString } - /** - * Estimates the gas cost needed to bridge the message that results from a transaction. - * @internal - */ - _estimate() { - return this._root._query(['estimate', this.chainId], () => - defer(() => { - const bytes = toHex(new Uint8Array([0x12])) - const { centrifugeRouter } = lpConfig[this.chainId]! - return this._root.getClient(this.chainId)!.readContract({ - address: centrifugeRouter, - abi: ABI.CentrifugeRouter, - functionName: 'estimate', - args: [bytes], - }) as Promise - }) - ) - } - - /** - * Get the contract address of the share token. - * @internal - */ - _share() { - return this._query(['share'], () => - defer( - () => - this._root.getClient(this.chainId)!.readContract({ - address: this.address, - abi: ABI.LiquidityPool, - functionName: 'share', - }) as Promise - ) - ) - } - /** * Get the contract address of the restriction mananger. * @internal */ _restrictionManager() { return this._query(['restrictionManager'], () => - this._share().pipe( - switchMap((share) => - defer( - () => - this._root.getClient(this.chainId)!.readContract({ - address: share, - abi: ABI.Currency, - functionName: 'hook', - }) as Promise - ) + this.network._share(this.trancheId).pipe( + switchMap( + (share) => + this._root.getClient(this.chainId)!.readContract({ + address: share, + abi: ABI.Currency, + functionName: 'hook', + }) as Promise ) ) ) @@ -102,14 +64,14 @@ export class Vault extends Entity { * Get the details of the investment currency. */ investmentCurrency() { - return this._query(null, () => this._root.currency(this._asset, this.chainId)) + return this._root.currency(this._asset, this.chainId) } /** * Get the details of the share token. */ shareCurrency() { - return this._query(null, () => this._share().pipe(switchMap((share) => this._root.currency(share, this.chainId)))) + return this.network.shareCurrency(this.trancheId) } /** @@ -241,9 +203,8 @@ export class Vault extends Entity { 'RedeemRequest', 'Withdraw', ], - filter: (events) => { - console.log('events', events) - return events.some( + filter: (events) => + events.some( (event) => event.args.receiver?.toLowerCase() === address || event.args.controller?.toLowerCase() === address || @@ -252,8 +213,7 @@ export class Vault extends Entity { // UpdateMember event (event.args.user?.toLowerCase() === address && event.args.token?.toLowerCase() === shareCurrency.address) - ) - }, + ), }, this.chainId ) @@ -271,42 +231,23 @@ export class Vault extends Entity { } /** - * Place an order to invest funds in the vault. If the amount is 0, it will request to cancel an open order. + * Place an order to invest funds in the vault. If an order exists, it will increase the amount. * @param investAmount - The amount to invest in the vault */ - placeInvestOrder(investAmount: bigint | number) { + increaseInvestOrder(investAmount: bigint | number) { const self = this return this._transactSequence(async function* ({ walletClient, publicClient, signer, signingAddress }) { const { centrifugeRouter } = lpConfig[self.chainId]! - const [estimate, investment] = await Promise.all([self._estimate(), self.investment(signingAddress)]) + const [estimate, investment] = await Promise.all([self.network._estimate(), self.investment(signingAddress)]) const amount = new Currency(investAmount, investment.investmentCurrency.decimals) - const { - investmentCurrency, - investmentCurrencyBalance, - investmentCurrencyAllowance, - isAllowedToInvest, - pendingInvestCurrency, - } = investment + const { investmentCurrency, investmentCurrencyBalance, investmentCurrencyAllowance, isAllowedToInvest } = + investment const supportsPermit = investmentCurrency.supportsPermit && 'send' in signer // eth-permit uses the deprecated send method const needsApproval = investmentCurrencyAllowance.lt(amount) if (!isAllowedToInvest) throw new Error('Not allowed to invest') - if (amount.isZero() && pendingInvestCurrency.isZero()) throw new Error('No order to cancel') if (amount.gt(investmentCurrencyBalance)) throw new Error('Insufficient balance') - if (pendingInvestCurrency.gt(0n) && amount.gt(0n)) throw new Error('Cannot change order') - - if (amount.isZero()) { - yield* doTransaction('Cancel Invest Order', publicClient, () => - walletClient.writeContract({ - address: centrifugeRouter, - abi: ABI.CentrifugeRouter, - functionName: 'cancelDepositRequest', - args: [self.address, estimate], - value: estimate, - }) - ) - return - } + if (!amount.gt(0n)) throw new Error('Order amount must be greater than 0') let permit: Permit | null = null if (needsApproval) { @@ -375,36 +316,39 @@ export class Vault extends Entity { * Cancel an open investment order. */ cancelInvestOrder() { - return this.placeInvestOrder(0n) + const self = this + return this._transactSequence(async function* ({ walletClient, signingAddress, publicClient }) { + const { centrifugeRouter } = lpConfig[self.chainId]! + const [estimate, investment] = await Promise.all([self.network._estimate(), self.investment(signingAddress)]) + + if (investment.pendingInvestCurrency.isZero()) throw new Error('No order to cancel') + + yield* doTransaction('Cancel Invest Order', publicClient, () => + walletClient.writeContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName: 'cancelDepositRequest', + args: [self.address, estimate], + value: estimate, + }) + ) + }, this.chainId) } /** - * Place an order to redeem funds from the vault. If the amount is 0, it will request to cancel an open order. + * Place an order to redeem funds from the vault. If an order exists, it will increase the amount. * @param shares - The amount of shares to redeem */ - placeRedeemOrder(shares: bigint | number) { + increaseRedeemOrder(shares: bigint | number) { const self = this - return this._transactSequence(async function* ({ walletClient, publicClient, signingAddress }) { + return this._transactSequence(async function* ({ walletClient, signingAddress, publicClient }) { const { centrifugeRouter } = lpConfig[self.chainId]! - const [estimate, investment] = await Promise.all([self._estimate(), self.investment(signingAddress)]) - const { shareBalance, pendingRedeemShares } = investment + const [estimate, investment] = await Promise.all([self.network._estimate(), self.investment(signingAddress)]) const amount = new Token(shares, investment.shareCurrency.decimals) - if (amount.isZero() && pendingRedeemShares.isZero()) throw new Error('No order to cancel') - if (amount.gt(shareBalance)) throw new Error('Insufficient balance') - if (pendingRedeemShares.gt(0n) && amount.gt(0n)) throw new Error('Cannot change order') - if (amount.isZero()) { - yield* doTransaction('Cancel Redeem Order', publicClient, () => - walletClient.writeContract({ - address: centrifugeRouter, - abi: ABI.CentrifugeRouter, - functionName: 'cancelRedeemRequest', - args: [self.address, estimate], - value: estimate, - }) - ) - return - } + if (amount.gt(investment.shareBalance)) throw new Error('Insufficient balance') + if (!amount.gt(0n)) throw new Error('Order amount must be greater than 0') + yield* doTransaction('Redeem', publicClient, () => walletClient.writeContract({ address: centrifugeRouter, @@ -421,7 +365,23 @@ export class Vault extends Entity { * Cancel an open redemption order. */ cancelRedeemOrder() { - return this.placeRedeemOrder(0n) + const self = this + return this._transactSequence(async function* ({ walletClient, signingAddress, publicClient }) { + const { centrifugeRouter } = lpConfig[self.chainId]! + const [estimate, investment] = await Promise.all([self.network._estimate(), self.investment(signingAddress)]) + + if (investment.pendingRedeemShares.isZero()) throw new Error('No order to cancel') + + yield* doTransaction('Cancel Redeem Order', publicClient, () => + walletClient.writeContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName: 'cancelRedeemRequest', + args: [self.address, estimate], + value: estimate, + }) + ) + }, this.chainId) } /** @@ -429,9 +389,10 @@ export class Vault extends Entity { * @param receiver - The address that should receive the funds. If not provided, the investor's address is used. */ claim(receiver?: string) { - return this._transact('Claim', async ({ walletClient, signingAddress }) => { - const { centrifugeRouter } = lpConfig[this.chainId]! - const investment = await this.investment(signingAddress) + const self = this + return this._transactSequence(async function* ({ walletClient, signingAddress, publicClient }) { + const { centrifugeRouter } = lpConfig[self.chainId]! + const investment = await self.investment(signingAddress) const receiverAddress = receiver || signingAddress const functionName = investment.claimableCancelInvestCurrency.gt(0n) ? 'claimCancelDepositRequest' @@ -445,12 +406,14 @@ export class Vault extends Entity { if (!functionName) throw new Error('No claimable funds') - return walletClient.writeContract({ - address: centrifugeRouter, - abi: ABI.CentrifugeRouter, - functionName, - args: [this.address, receiverAddress, signingAddress], - }) + yield* doTransaction('Claim', publicClient, () => + walletClient.writeContract({ + address: centrifugeRouter, + abi: ABI.CentrifugeRouter, + functionName, + args: [self.address, receiverAddress, signingAddress], + }) + ) }) } } diff --git a/src/utils/rx.ts b/src/utils/rx.ts index fe5b98b..3a0911e 100644 --- a/src/utils/rx.ts +++ b/src/utils/rx.ts @@ -51,8 +51,8 @@ export function makeThenable($query: Observable, exhaust = false) { return thenableQuery } +// A ReplaySubject that completes when an existing buffer is expired class ExpiringReplaySubject extends ReplaySubject { - // Re-implementation of ReplaySubject._subscribe that completes the subject when an existing buffer is expired // @ts-expect-error protected override _subscribe(subscriber: Subscriber): Subscription { // @ts-expect-error From 518bcf584f4bff182397b63a51ffa0b0c557dee3 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:24:42 +0100 Subject: [PATCH 52/53] allow decimalwrapper as input --- src/Vault.ts | 18 +++++++++++++----- src/utils/BigInt.ts | 14 +++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Vault.ts b/src/Vault.ts index 68d6fad..ca9587a 100644 --- a/src/Vault.ts +++ b/src/Vault.ts @@ -1,3 +1,4 @@ +import { Decimal } from 'decimal.js-light' import { combineLatest, defer, map, switchMap } from 'rxjs' import { encodeFunctionData, getContract } from 'viem' import type { Centrifuge } from './Centrifuge.js' @@ -7,7 +8,7 @@ import { PoolNetwork } from './PoolNetwork.js' import { ABI } from './abi/index.js' import { lpConfig } from './config/lp.js' import type { HexString } from './types/index.js' -import { Currency, Token } from './utils/BigInt.js' +import { Currency, DecimalWrapper, Token } from './utils/BigInt.js' import { repeatOnEvents } from './utils/rx.js' import { doSignMessage, doTransaction, signPermit, type Permit } from './utils/transaction.js' @@ -234,12 +235,12 @@ export class Vault extends Entity { * Place an order to invest funds in the vault. If an order exists, it will increase the amount. * @param investAmount - The amount to invest in the vault */ - increaseInvestOrder(investAmount: bigint | number) { + increaseInvestOrder(investAmount: NumberInput) { const self = this return this._transactSequence(async function* ({ walletClient, publicClient, signer, signingAddress }) { const { centrifugeRouter } = lpConfig[self.chainId]! const [estimate, investment] = await Promise.all([self.network._estimate(), self.investment(signingAddress)]) - const amount = new Currency(investAmount, investment.investmentCurrency.decimals) + const amount = toCurrency(investAmount, investment.investmentCurrency.decimals) const { investmentCurrency, investmentCurrencyBalance, investmentCurrencyAllowance, isAllowedToInvest } = investment const supportsPermit = investmentCurrency.supportsPermit && 'send' in signer // eth-permit uses the deprecated send method @@ -339,12 +340,12 @@ export class Vault extends Entity { * Place an order to redeem funds from the vault. If an order exists, it will increase the amount. * @param shares - The amount of shares to redeem */ - increaseRedeemOrder(shares: bigint | number) { + increaseRedeemOrder(shares: NumberInput) { const self = this return this._transactSequence(async function* ({ walletClient, signingAddress, publicClient }) { const { centrifugeRouter } = lpConfig[self.chainId]! const [estimate, investment] = await Promise.all([self.network._estimate(), self.investment(signingAddress)]) - const amount = new Token(shares, investment.shareCurrency.decimals) + const amount = toCurrency(shares, investment.shareCurrency.decimals) if (amount.gt(investment.shareBalance)) throw new Error('Insufficient balance') if (!amount.gt(0n)) throw new Error('Order amount must be greater than 0') @@ -417,3 +418,10 @@ export class Vault extends Entity { }) } } + +type NumberInput = number | bigint | DecimalWrapper | Decimal +function toCurrency(val: NumberInput, decimals: number) { + return typeof val === 'number' + ? Currency.fromFloat(val, decimals!) + : new Currency(val instanceof DecimalWrapper ? val.toBigInt() : val, decimals) +} diff --git a/src/utils/BigInt.ts b/src/utils/BigInt.ts index 3f3640b..47d8e71 100644 --- a/src/utils/BigInt.ts +++ b/src/utils/BigInt.ts @@ -1,14 +1,14 @@ -import Decimal, { type Numeric } from 'decimal.js-light' +import { Decimal, type Numeric } from 'decimal.js-light' -Decimal.default.set({ +Decimal.set({ precision: 30, toExpNeg: -7, toExpPos: 29, - rounding: Decimal.default.ROUND_HALF_CEIL, // ROUND_HALF_CEIL is 1 + rounding: Decimal.ROUND_HALF_CEIL, // ROUND_HALF_CEIL is 1 }) export function Dec(value: Numeric) { - return new Decimal.default(value) + return new Decimal(value) } export abstract class BigIntWrapper { @@ -17,7 +17,7 @@ export abstract class BigIntWrapper { constructor(value: Numeric | bigint) { if (typeof value === 'bigint') { this.value = value - } else if (value instanceof Decimal.default) { + } else if (value instanceof Decimal) { this.value = BigInt(value.toFixed(0)) } else if (typeof value === 'number') { this.value = BigInt(Math.floor(value)) @@ -77,10 +77,10 @@ export class DecimalWrapper extends BigIntWrapper { * // returns Currency with 6 decimals (1_010_000n or 1.01) */ _mul(value: bigint | (T extends DecimalWrapper ? T : never)): T { - let val: Decimal.default + let val: Decimal if (typeof value === 'bigint') { val = Dec(value.toString()) - } else if (value instanceof Decimal.default) { + } else if (value instanceof Decimal) { val = value } else { val = value.toDecimal().mul(Dec(10).pow(this.decimals)) From fbdfd6cf04afa413022b45a287c14a225f3173ef Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:16:52 +0100 Subject: [PATCH 53/53] feedback --- src/PoolNetwork.ts | 4 ++-- src/Vault.ts | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/PoolNetwork.ts b/src/PoolNetwork.ts index b68def7..7e41320 100644 --- a/src/PoolNetwork.ts +++ b/src/PoolNetwork.ts @@ -308,7 +308,7 @@ export class PoolNetwork extends Entity { self.canTrancheBeDeployed(trancheId), ]) if (!canTrancheBeDeployed) throw new Error('Pool is not active on this network') - yield* doTransaction('Deploy Tranche', publicClient, () => + yield* doTransaction('Deploy tranche', publicClient, () => walletClient.writeContract({ address: poolManager, abi: ABI.PoolManager, @@ -329,7 +329,7 @@ export class PoolNetwork extends Entity { return this._transactSequence(async function* ({ walletClient, publicClient }) { const [poolManager, trancheToken] = await Promise.all([self._poolManager(), self._share(trancheId)]) if (!trancheToken) throw new Error('Pool is not active on this network') - yield* doTransaction('Deploy Vault', publicClient, () => + yield* doTransaction('Deploy vault', publicClient, () => walletClient.writeContract({ address: poolManager, abi: ABI.PoolManager, diff --git a/src/Vault.ts b/src/Vault.ts index ca9587a..4e2057a 100644 --- a/src/Vault.ts +++ b/src/Vault.ts @@ -324,7 +324,7 @@ export class Vault extends Entity { if (investment.pendingInvestCurrency.isZero()) throw new Error('No order to cancel') - yield* doTransaction('Cancel Invest Order', publicClient, () => + yield* doTransaction('Cancel invest order', publicClient, () => walletClient.writeContract({ address: centrifugeRouter, abi: ABI.CentrifugeRouter, @@ -338,14 +338,14 @@ export class Vault extends Entity { /** * Place an order to redeem funds from the vault. If an order exists, it will increase the amount. - * @param shares - The amount of shares to redeem + * @param redeemAmount - The amount of shares to redeem */ - increaseRedeemOrder(shares: NumberInput) { + increaseRedeemOrder(redeemAmount: NumberInput) { const self = this return this._transactSequence(async function* ({ walletClient, signingAddress, publicClient }) { const { centrifugeRouter } = lpConfig[self.chainId]! const [estimate, investment] = await Promise.all([self.network._estimate(), self.investment(signingAddress)]) - const amount = toCurrency(shares, investment.shareCurrency.decimals) + const amount = toCurrency(redeemAmount, investment.shareCurrency.decimals) if (amount.gt(investment.shareBalance)) throw new Error('Insufficient balance') if (!amount.gt(0n)) throw new Error('Order amount must be greater than 0') @@ -373,7 +373,7 @@ export class Vault extends Entity { if (investment.pendingRedeemShares.isZero()) throw new Error('No order to cancel') - yield* doTransaction('Cancel Redeem Order', publicClient, () => + yield* doTransaction('Cancel redeem order', publicClient, () => walletClient.writeContract({ address: centrifugeRouter, abi: ABI.CentrifugeRouter, @@ -388,13 +388,16 @@ export class Vault extends Entity { /** * Claim any outstanding fund shares after an investment has gone through, or funds after an redemption has gone through. * @param receiver - The address that should receive the funds. If not provided, the investor's address is used. + * @param controller - The address of the user that has invested. Allows someone else to claim on behalf of the user + * if the user has set the CentrifugeRouter as an operator on the vault. If not provided, the investor's address is used. */ - claim(receiver?: string) { + claim(receiver?: string, controller?: string) { const self = this return this._transactSequence(async function* ({ walletClient, signingAddress, publicClient }) { const { centrifugeRouter } = lpConfig[self.chainId]! const investment = await self.investment(signingAddress) const receiverAddress = receiver || signingAddress + const controllerAddress = controller || signingAddress const functionName = investment.claimableCancelInvestCurrency.gt(0n) ? 'claimCancelDepositRequest' : investment.claimableCancelRedeemShares.gt(0n) @@ -412,7 +415,7 @@ export class Vault extends Entity { address: centrifugeRouter, abi: ABI.CentrifugeRouter, functionName, - args: [self.address, receiverAddress, signingAddress], + args: [self.address, receiverAddress, controllerAddress], }) ) })