Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Liquidity Pools methods #19

Merged
merged 57 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
e8b88bd
queries
onnovisser Oct 15, 2024
f901413
abi
onnovisser Oct 15, 2024
8c8a0d2
events and subquery
onnovisser Oct 17, 2024
157226f
a file
onnovisser Oct 17, 2024
2e3a348
Update types for querySubquery
sophialittlejohn Oct 17, 2024
1d9e7b4
subquery
onnovisser Oct 18, 2024
5a489c8
Add testing setup with mocha/chai using tenderly virtual textnet rpc
sophialittlejohn Oct 18, 2024
a3bf57b
Fix accounts testing by switching to demo
sophialittlejohn Oct 18, 2024
a51165d
transact
onnovisser Oct 21, 2024
e4d578a
readme
onnovisser Oct 21, 2024
e8baced
fixes and comments
onnovisser Oct 22, 2024
5484a71
Merge branch 'queries' of github.com:centrifuge/centrifuge-sdk into t…
sophialittlejohn Oct 22, 2024
1528303
Map rpc urls to chain id
sophialittlejohn Oct 22, 2024
7ad72e0
Create tenderly virtual network for test suite
sophialittlejohn Oct 22, 2024
c7159bd
Refactor for easier reuse
sophialittlejohn Oct 22, 2024
26affb5
Remove unused package
sophialittlejohn Oct 22, 2024
8a433b4
Add readme
sophialittlejohn Oct 23, 2024
c1f54f2
split transact function
onnovisser Oct 24, 2024
8ae7c1f
Increase timeout for api requests
sophialittlejohn Oct 24, 2024
795da27
Make tenderly class more robust and add signing functionality
sophialittlejohn Oct 24, 2024
403c475
Clean up TenderlyFork setup work by adding static create method
sophialittlejohn Oct 24, 2024
389a264
Update readme
sophialittlejohn Oct 24, 2024
08ccd88
export types
onnovisser Oct 25, 2024
8d38348
merge tests
onnovisser Oct 25, 2024
752b0b4
transfer test
onnovisser Oct 25, 2024
e49c6f6
query tests
onnovisser Nov 1, 2024
4ea1ec7
Extract setup of Centrifuge(Test) and TenderlyFork into Context (#9)
sophialittlejohn Nov 4, 2024
e8e91e3
tx test
onnovisser Nov 6, 2024
e5ba56e
cleanup
onnovisser Nov 12, 2024
eb683b7
config
onnovisser Nov 12, 2024
17c6921
feedback
onnovisser Nov 12, 2024
3a82755
Impersonation for tests (overwrite from address in transactions) (#18)
sophialittlejohn Nov 12, 2024
5e6ec9e
rename
onnovisser Nov 13, 2024
24d5ca3
readme
onnovisser Nov 13, 2024
1627041
cleanup
onnovisser Nov 13, 2024
5a6f894
comments
onnovisser Nov 13, 2024
e40e655
fixes
onnovisser Nov 14, 2024
ce8abc6
cleanup
onnovisser Nov 14, 2024
deff723
brackets
onnovisser Nov 15, 2024
4977f9e
fix keys
onnovisser Nov 15, 2024
55d73cf
add vault entity
onnovisser Nov 15, 2024
c73e82c
comments
onnovisser Nov 15, 2024
e1d6cbe
Merge branch 'queries' into lp
onnovisser Nov 15, 2024
de54014
tests
onnovisser Nov 20, 2024
fe60150
merge main
onnovisser Nov 20, 2024
be5510c
big number types
onnovisser Nov 21, 2024
006ef26
fix tests
onnovisser Nov 21, 2024
2b033a6
more tests
onnovisser Nov 26, 2024
333f3c4
fix path
onnovisser Nov 26, 2024
06d2c1d
fix cache expiry
onnovisser Nov 26, 2024
44493cc
fix share
onnovisser Nov 26, 2024
03f95d1
simplify and fix tests
onnovisser Nov 26, 2024
98a8491
remove log
onnovisser Nov 26, 2024
79b8572
comments
onnovisser Nov 26, 2024
2e5ea2d
fix tests
onnovisser Nov 29, 2024
518bcf5
allow decimalwrapper as input
onnovisser Dec 2, 2024
fbdfd6c
feedback
onnovisser Dec 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 92 additions & 6 deletions src/Centrifuge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import {
mergeMap,
of,
Subject,
switchMap,
using,
} from 'rxjs'
import { fromFetch } from 'rxjs/fetch'
import {
createPublicClient,
createWalletClient,
custom,
getContract,
http,
parseEventLogs,
type Abi,
Expand All @@ -27,14 +29,18 @@ 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, serializeForCache } from './utils/query.js'
import { makeThenable, shareReplayWithDelayedReset } from './utils/rx.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'

export type Config = {
Expand Down Expand Up @@ -132,6 +138,83 @@ 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<CurrencyMetadata> {
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<number>,
contract.read.name!() as Promise<string>,
contract.read.symbol!() as Promise<string>,
contract.read.PERMIT_TYPEHASH!()
.then((hash) => hash === PERMIT_TYPEHASH)
.catch(() => false),
])
return {
address: curAddress as any,
decimals,
name,
symbol,
chainId: cid,
supportsPermit,
}
})
)
}

/**
* 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
return this._query(['balance', currency, owner, cid], () => {
return this.currency(currency, cid).pipe(
switchMap((currencyMeta) =>
defer(() =>
this.getClient(cid)!
.readContract({
address: currency as any,
abi: ABI.Currency,
functionName: 'balanceOf',
args: [address],
})
.then((val: any) => new Currency(val, currencyMeta.decimals))
).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
Expand Down Expand Up @@ -226,7 +309,7 @@ export class Centrifuge {

#memoized = new Map<string, any>()
#memoizeWith<T = any>(keys: any[], callback: () => T): T {
const cacheKey = hashKey(serializeForCache(keys))
const cacheKey = hashKey(keys)
if (this.#memoized.has(cacheKey)) {
return this.#memoized.get(cacheKey)
}
Expand Down Expand Up @@ -332,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, which can happen with a finite `valueCacheTime`.
// 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()
Expand Down Expand Up @@ -430,7 +516,7 @@ export class Centrifuge {
params: TransactionCallbackParams
) => AsyncGenerator<OperationStatus> | Observable<OperationStatus>,
chainId?: number
) {
): Transaction {
const targetChainId = chainId ?? this.config.defaultChain
const self = this
async function* transact() {
Expand Down
20 changes: 20 additions & 0 deletions src/Pool.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
21 changes: 20 additions & 1 deletion src/Pool.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { catchError, combineLatest, map, of, switchMap, timeout } from 'rxjs'
import type { Centrifuge } from './Centrifuge.js'
import { Entity } from './Entity.js'
import { Reports } from './Reports/index.js'
import { PoolNetwork } from './PoolNetwork.js'
import { Reports } from './Reports/index.js'

export class Pool extends Entity {
constructor(
Expand Down Expand Up @@ -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.
*/
Expand All @@ -70,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))))
}
}
37 changes: 37 additions & 0 deletions src/PoolNetwork.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
80 changes: 79 additions & 1 deletion src/PoolNetwork.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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.
Expand Down Expand Up @@ -77,6 +78,83 @@ 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)
})
)
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.
Expand Down
Loading